diff --git a/.gitignore b/.gitignore index ea8c4bf..2955c66 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +db.sqlite diff --git a/Cargo.lock b/Cargo.lock index 4987986..5526c20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,6 +23,32 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -137,6 +163,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + [[package]] name = "byteorder" version = "1.5.0" @@ -164,6 +196,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -179,6 +226,23 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -239,6 +303,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "digest" version = "0.10.7" @@ -359,6 +433,20 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -403,6 +491,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -423,6 +522,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -610,6 +710,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -765,6 +889,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -817,6 +951,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", + "serde", ] [[package]] @@ -914,6 +1049,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -1042,6 +1183,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1396,6 +1543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0" dependencies = [ "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1475,6 +1623,7 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -1516,6 +1665,7 @@ dependencies = [ "base64", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -1550,6 +1700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -1577,11 +1728,13 @@ name = "stationslauf" version = "0.1.0" dependencies = [ "axum", + "chrono", "dotenv", "maud", "serde", "sqlx", "tokio", + "tower-sessions", "tracing", ] @@ -1663,6 +1816,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1742,6 +1926,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-cookies" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" +dependencies = [ + "axum-core", + "cookie", + "futures-util", + "http", + "parking_lot", + "pin-project-lite", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -1754,6 +1954,57 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" +[[package]] +name = "tower-sessions" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3" +dependencies = [ + "async-trait", + "http", + "time", + "tokio", + "tower-cookies", + "tower-layer", + "tower-service", + "tower-sessions-core", + "tower-sessions-memory-store", + "tracing", +] + +[[package]] +name = "tower-sessions-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b" +dependencies = [ + "async-trait", + "axum-core", + "base64", + "futures", + "http", + "parking_lot", + "rand", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tracing", +] + +[[package]] +name = "tower-sessions-memory-store" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242" +dependencies = [ + "async-trait", + "time", + "tokio", + "tower-sessions-core", +] + [[package]] name = "tracing" version = "0.1.41" @@ -1881,6 +2132,64 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -1900,6 +2209,65 @@ dependencies = [ "wasite", ] +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 8265450..4787316 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,11 @@ edition = "2024" [dependencies] axum = "0.8" +chrono = { version = "0.4", features = ["serde"]} dotenv = "0.15" maud = { version = "0.27", features = ["axum"] } serde = "1.0" -sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros"] } +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] } tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] } +tower-sessions = "0.14" tracing = "0.1" - diff --git a/README.md b/README.md index 2f5f9bc..747675d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ - HTMX as frontend +## Next steps + +- [ ] Station + - [ ] single-detail view: show all attributes (amount_people, last_login, pw, lat, lng) + make updateable + ## Fancy features - see when a group starts going to your direction - QR codes for groups, stations can then scan it? diff --git a/db.sqlite b/db.sqlite index c9d2a8c..18cdbb4 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/migrations.sqlite b/migration.sql similarity index 82% rename from migrations.sqlite rename to migration.sql index 01bc94b..868a47a 100644 --- a/migrations.sqlite +++ b/migration.sql @@ -1,9 +1,9 @@ CREATE TABLE station ( id INTEGER PRIMARY KEY, - name TEXT NOT NULL, + name TEXT NOT NULL UNIQUE, amount_people INTEGER, - last_login TIMESTAMP, - pw TEXT NOT NULL, + last_login DATETIME, + pw TEXT NOT NULL DEFAULT (upper(hex(randomblob(4)))), lat REAL, lng REAL ); @@ -23,9 +23,9 @@ CREATE TABLE group_station ( station_id INTEGER, points INTEGER, notes TEXT, - arrived_at TIMESTAMP, - started_at TIMESTAMP, - left_at TIMESTAMP, + arrived_at DATETIME, + started_at DATETIME, + left_at DATETIME, PRIMARY KEY (group_id, station_id), FOREIGN KEY (group_id) REFERENCES "group"(id), FOREIGN KEY (station_id) REFERENCES station(id) diff --git a/src/lib.rs b/src/lib.rs index 44d930c..3a99f65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,14 +2,19 @@ use axum::Router; use sqlx::SqlitePool; use std::sync::Arc; use tokio::net::TcpListener; +use tower_sessions::{MemoryStore, SessionManagerLayer}; mod station; /// Starts the main application. pub async fn start(listener: TcpListener, db: SqlitePool) { + let session_store = MemoryStore::default(); + let session_layer = SessionManagerLayer::new(session_store); + let app = Router::new() .nest("/station", station::routes()) - .with_state(Arc::new(db)); + .with_state(Arc::new(db)) + .layer(session_layer); axum::serve(listener, app).await.unwrap(); } diff --git a/src/station.rs b/src/station.rs deleted file mode 100644 index 57144bb..0000000 --- a/src/station.rs +++ /dev/null @@ -1,43 +0,0 @@ -use axum::{extract::State, routing::get, Router}; -use maud::{html, Markup}; -use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, SqlitePool}; -use std::sync::Arc; - -#[derive(FromRow, Debug, Serialize, Deserialize)] -struct Station { - id: u64, - name: String, - amount_people: u8, - last_login: Option, // TODO use proper timestamp (NaiveDateTime?) - pw: String, - lat: Option, - lng: Option, -} - -impl Station { - async fn all(db: &SqlitePool) -> Vec { - sqlx::query_as::<_, Self>( - "SELECT id, name, amount_people, last_login, pw, lat, lng FROM station;", - ) - .fetch_all(db) - .await - .unwrap() - } -} - -async fn get_stations(State(db): State>) -> Markup { - let all = Station::all(&db).await; - let mut ret = String::new(); - for a in all { - ret.push_str(&a.name); - } - - html! { - div { (ret) } - } -} - -pub(super) fn routes() -> Router> { - Router::new().route("/", get(get_stations)) -} diff --git a/src/station/mod.rs b/src/station/mod.rs new file mode 100644 index 0000000..5669cda --- /dev/null +++ b/src/station/mod.rs @@ -0,0 +1,59 @@ +use axum::Router; +use chrono::NaiveDateTime; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, SqlitePool}; +use std::sync::Arc; + +mod routes; + +#[derive(FromRow, Debug, Serialize, Deserialize)] +struct Station { + id: i64, + name: String, + amount_people: Option, + last_login: Option, // TODO use proper timestamp (NaiveDateTime?) + pw: String, + lat: Option, + lng: Option, +} + +impl Station { + async fn all(db: &SqlitePool) -> Vec { + sqlx::query_as::<_, Self>( + "SELECT id, name, amount_people, last_login, pw, lat, lng FROM station;", + ) + .fetch_all(db) + .await + .unwrap() + } + + pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option { + sqlx::query_as!( + Self, + "SELECT id, name, amount_people, last_login, pw, lat, lng FROM station WHERE id like ?", + id + ) + .fetch_one(db) + .await + .ok() + } + + async fn create(db: &SqlitePool, name: &str) -> Result<(), String> { + sqlx::query!("INSERT INTO station(name) VALUES (?)", name) + .execute(db) + .await + .map_err(|e| e.to_string())?; + Ok(()) + } + + async fn delete(&self, db: &SqlitePool) { + sqlx::query!("DELETE FROM station WHERE id = ?", self.id) + .execute(db) + .await + .unwrap(); + } +} + +pub(super) fn routes() -> Router> { + routes::routes() +} diff --git a/src/station/routes.rs b/src/station/routes.rs new file mode 100644 index 0000000..9f2614c --- /dev/null +++ b/src/station/routes.rs @@ -0,0 +1,114 @@ +use crate::station::Station; +use axum::{ + extract::State, + response::{IntoResponse, Redirect}, + routing::{get, post}, + Form, Router, +}; +use maud::{html, Markup}; +use serde::Deserialize; +use sqlx::SqlitePool; +use std::sync::Arc; +use tower_sessions::Session; + +#[derive(Deserialize)] +struct CreateForm { + name: String, +} + +async fn create( + State(db): State>, + session: Session, + Form(form): Form, +) -> impl IntoResponse { + Station::create(&db, &form.name).await.unwrap(); + + session + .insert( + "succ", + &format!("Station '{}' erfolgreich erstellt!", form.name), + ) + .await + .unwrap(); + + Redirect::to("/station") +} + +async fn delete( + State(db): State>, + session: Session, + axum::extract::Path(id): axum::extract::Path, +) -> impl IntoResponse { + let Some(station) = Station::find_by_id(&db, id).await else { + session + .insert( + "err", + &format!( + "Station mit ID {id} konnte nicht gelöscht werden, da sie nicht existiert" + ), + ) + .await + .unwrap(); + + return Redirect::to("/station"); + }; + + station.delete(&db).await; + + session + .insert( + "succ", + &format!("Station '{}' erfolgreich gelöscht!", station.name), + ) + .await + .unwrap(); + + Redirect::to("/station") +} + +async fn index(State(db): State>, session: Session) -> Markup { + let stations = Station::all(&db).await; + + // Get and clear flash message + let flash_message = session.get::("succ").await.unwrap_or(None); + if flash_message.is_some() { + session.remove::("succ").await.unwrap(); + } + + html! { + h1 { "Stationen" } + @if let Some(message) = flash_message { + div class="alert alert-success" { + (message) + } + } + ol { + @for station in &stations { + li { + (station.name) + a href=(format!("/station/{}/delete", station.id)) + onclick="return confirm('Bist du sicher, dass die Station gelöscht werden soll? Das kann _NICHT_ mehr rückgängig gemacht werden.');" { + "🗑️" + } + } + } + } + h2 { "Neue Station" } + form action="/station" method="post" { + div { + label for="name" { "Name:" } + input type="text" name="name" required; + } + div { + button type="submit" { "Submit" } + } + } + } +} + +pub(super) fn routes() -> Router> { + Router::new() + .route("/", get(index)) + .route("/{id}/delete", get(delete)) + .route("/", post(create)) +}