diff --git a/Cargo.lock b/Cargo.lock index 7efe188..bd54ca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -274,6 +309,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -295,7 +340,11 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ + "aes-gcm", + "base64", "percent-encoding", + "rand", + "subtle", "time", "version_check", ] @@ -371,9 +420,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "der" version = "0.7.9" @@ -646,6 +705,16 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1011,6 +1080,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "itertools" version = "0.11.0" @@ -1151,9 +1229,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] @@ -1246,6 +1324,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking" version = "2.2.1" @@ -1329,6 +1413,18 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2383,6 +2479,16 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/Cargo.toml b/Cargo.toml index 01c74cf..26e833e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] axum = "0.8" -axum-extra = { version = "0.10", features = [ "cookie" ]} +axum-extra = { version = "0.10", features = [ "cookie", "cookie-private" ]} chrono = { version = "0.4", features = ["serde"]} dotenv = "0.15" maud = { version = "0.27", features = ["axum"] } diff --git a/src/admin/mod.rs b/src/admin/mod.rs index c8c2255..e798457 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -1,8 +1,6 @@ -use crate::page; +use crate::{page, AppState}; use axum::{routing::get, Router}; use maud::{html, Markup}; -use sqlx::SqlitePool; -use std::sync::Arc; use tower_sessions::Session; pub(crate) mod route; @@ -35,7 +33,7 @@ async fn index(session: Session) -> Markup { page(content, session, false).await } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { Router::new() .route("/", get(index)) .nest("/station", station::routes()) diff --git a/src/admin/route/mod.rs b/src/admin/route/mod.rs index 051f6d5..1280824 100644 --- a/src/admin/route/mod.rs +++ b/src/admin/route/mod.rs @@ -1,8 +1,10 @@ -use crate::admin::{station::Station, team::Team}; +use crate::{ + admin::{station::Station, team::Team}, + AppState, +}; use axum::Router; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Row, SqlitePool}; -use std::sync::Arc; mod web; @@ -195,6 +197,6 @@ DROP TABLE temp_pos;", } } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { web::routes() } diff --git a/src/admin/route/web.rs b/src/admin/route/web.rs index 596c40e..606aedc 100644 --- a/src/admin/route/web.rs +++ b/src/admin/route/web.rs @@ -1,5 +1,5 @@ use super::Route; -use crate::{admin::station::Station, err, page, succ}; +use crate::{admin::station::Station, err, page, succ, AppState}; use axum::{ extract::State, response::{IntoResponse, Redirect}, @@ -53,7 +53,7 @@ async fn index(State(db): State>, session: Session) -> Markup { } h2 { "Neue Route" } form action="/admin/route" method="post" { - fieldset role="team" { + fieldset role="group" { input type="text" name="name" placeholder="Routenname" required; input type="submit" value="Neue Route"; } @@ -374,7 +374,7 @@ async fn move_station_higher( Redirect::to(&format!("/admin/route/{route_id}")) } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { Router::new() .route("/", get(index)) .route("/", post(create)) diff --git a/src/admin/station/mod.rs b/src/admin/station/mod.rs index d803ac5..ace13df 100644 --- a/src/admin/station/mod.rs +++ b/src/admin/station/mod.rs @@ -1,9 +1,8 @@ -use crate::admin::route::Route; +use crate::{admin::route::Route, AppState}; use axum::Router; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; -use std::sync::Arc; mod web; @@ -14,7 +13,7 @@ pub(crate) struct Station { notes: Option, amount_people: Option, last_login: Option, // TODO use proper timestamp (NaiveDateTime?) - pw: String, + pub(crate) pw: String, lat: Option, lng: Option, } @@ -139,6 +138,6 @@ impl Station { } } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { web::routes() } diff --git a/src/admin/station/web.rs b/src/admin/station/web.rs index bf57b29..493a00a 100644 --- a/src/admin/station/web.rs +++ b/src/admin/station/web.rs @@ -1,4 +1,4 @@ -use crate::{admin::station::Station, er, err, partials::page, suc, succ}; +use crate::{admin::station::Station, er, err, partials::page, suc, succ, AppState}; use axum::{ extract::State, response::{IntoResponse, Redirect}, @@ -450,7 +450,7 @@ async fn index(State(db): State>, session: Session) -> Markup { } h2 { (t!("station_new")) } form action="/admin/station" method="post" { - fieldset role="team" { + fieldset role="group" { input type="text" name="name" placeholder=(t!("station_name")) required; input type="submit" value=(t!("station_new")); } @@ -459,7 +459,7 @@ async fn index(State(db): State>, session: Session) -> Markup { page(content, session, false).await } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { Router::new() .route("/", get(index)) .route("/", post(create)) diff --git a/src/admin/team/mod.rs b/src/admin/team/mod.rs index 8189bed..84435ac 100644 --- a/src/admin/team/mod.rs +++ b/src/admin/team/mod.rs @@ -1,8 +1,10 @@ -use crate::admin::{route::Route, station::Station}; +use crate::{ + admin::{route::Route, station::Station}, + AppState, +}; use axum::Router; use serde::{Deserialize, Serialize}; use sqlx::{FromRow, SqlitePool}; -use std::sync::Arc; mod web; @@ -165,6 +167,6 @@ impl Team { } } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { web::routes() } diff --git a/src/admin/team/web.rs b/src/admin/team/web.rs index 8360a57..c91eb61 100644 --- a/src/admin/team/web.rs +++ b/src/admin/team/web.rs @@ -1,5 +1,10 @@ use super::{CreateError, Team}; -use crate::{admin::route::Route, admin::station::Station, err, partials::page, pl, succ}; +use crate::{ + admin::{route::Route, station::Station}, + err, + partials::page, + pl, succ, AppState, +}; use axum::{ extract::State, response::{IntoResponse, Redirect}, @@ -497,7 +502,7 @@ async fn index(State(db): State>, session: Session) -> Markup { } @else { form action="/admin/team" method="post" { @if routes.len() == 1 { - fieldset role="team" { + fieldset role="group" { input type="text" name="name" placeholder="Teamnamen" required; input type="hidden" name="route_id" value=(routes[0].id) ; input type="submit" value="Neues Team"; @@ -519,7 +524,7 @@ async fn index(State(db): State>, session: Session) -> Markup { page(content, session, false).await } -pub(super) fn routes() -> Router> { +pub(super) fn routes() -> Router { Router::new() .route("/", get(index)) .route("/", post(create)) diff --git a/src/lib.rs b/src/lib.rs index 1a3ac1a..cad2ac3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,12 @@ extern crate rust_i18n; i18n!("locales", fallback = "de-AT"); use admin::station::Station; -use axum::{body::Body, response::Response, routing::get, Router}; +use axum::{body::Body, extract::FromRef, response::Response, routing::get, Router}; use partials::page; use sqlx::SqlitePool; use std::sync::Arc; use tokio::net::TcpListener; -use tower_sessions::{MemoryStore, SessionManagerLayer}; +use tower_sessions::{cookie::Key, MemoryStore, SessionManagerLayer}; pub(crate) mod admin; mod partials; @@ -102,11 +102,34 @@ async fn serve_marker_png() -> Response { .unwrap() } +#[derive(Clone)] +struct AppState { + db: Arc, + key: Key, +} + +impl FromRef for Key { + fn from_ref(state: &AppState) -> Self { + state.key.clone() + } +} + +impl FromRef for Arc { + fn from_ref(state: &AppState) -> Self { + state.db.clone() + } +} + /// 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 state = AppState { + db: Arc::new(db), + key: Key::generate(), + }; + let app = Router::new() .nest("/s", station::routes()) // TODO: maybe switch to "/" .nest("/admin", admin::routes()) @@ -115,7 +138,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) { .route("/leaflet.css", get(serve_leaflet_css)) .route("/leaflet.js", get(serve_leaflet_js)) .route("/marker.png", get(serve_marker_png)) - .with_state(Arc::new(db)) + .with_state(state) .layer(session_layer); axum::serve(listener, app).await.unwrap(); diff --git a/src/station.rs b/src/station.rs index f9cbdcc..774c809 100644 --- a/src/station.rs +++ b/src/station.rs @@ -1,12 +1,13 @@ -use crate::{err, partials, succ, Station}; +use crate::{err, partials, succ, AppState, Station}; use axum::{ extract::State, response::{IntoResponse, Redirect}, - routing::get, - Router, + routing::{get, post}, + Form, Router, }; -use axum_extra::extract::CookieJar; +use axum_extra::extract::{CookieJar, PrivateCookieJar}; use maud::{html, Markup, PreEscaped}; +use serde::Deserialize; use sqlx::SqlitePool; use std::sync::Arc; use tower_sessions::{cookie::Cookie, Session}; @@ -91,8 +92,11 @@ async fn view( State(db): State>, session: Session, jar: CookieJar, + pjar: PrivateCookieJar, axum::extract::Path(id): axum::extract::Path, -) -> Result<(CookieJar, Markup), (CookieJar, impl IntoResponse)> { +) -> Result<(CookieJar, PrivateCookieJar, Markup), (CookieJar, PrivateCookieJar, impl IntoResponse)> +{ + // Station selector let (mut jar, current_station_cookie) = get_station_cookie(&db, jar).await; if current_station_cookie.is_none() { jar = jar.add(Cookie::new("station_id", id.to_string())); @@ -106,6 +110,7 @@ async fn view( err!(session, "Du hast versucht eine neue Station zu öffnen obwohl du bereits eine andere Station offen hattest. Welche möchtest du nun verwenden?"); return Ok(( jar, + pjar, decide_between_stations(¤t_station_cookie, &station, session).await, )); } else { @@ -113,12 +118,27 @@ async fn view( err!(session, "Du hast versucht eine Station öffnen, die es nicht gibt. Nachdem du vorher schonmal eine andere Station (die es gibt) geöffnet hattest, bist du nun zu dieser weitergeleitet worden. Wenn du das nicht willst, logg dich bitte aus."); return Err(( jar, + pjar, Redirect::to(&format!("/s/{}", current_station_cookie.id)), )); } } } let station = Station::find_by_id(&db, id).await.unwrap(); + + let mut pjar = pjar; + + // PW Checker + if let Some(pw) = pjar.get("pw") { + if pw.value() != station.pw { + pjar = pjar.remove(Cookie::from("station_id")); + err!(session, "Du hattest einen falschen Code für Station {} gespeichert. Bitte gibt den richtigen ein:", station.name ); + return Err((jar, pjar, Redirect::to(&format!("/s/code",)))); + } + } else { + return Err((jar, pjar, Redirect::to(&format!("/s/code",)))); + } + let content = html! { nav { ul { @@ -131,9 +151,75 @@ async fn view( h1 { "test" } }; - Ok((jar, partials::page(content, session, false).await)) + Ok((jar, pjar, partials::page(content, session, false).await)) } +async fn code( + State(db): State>, + session: Session, + jar: CookieJar, + pjar: PrivateCookieJar, +) -> Result<(CookieJar, PrivateCookieJar, Markup), (CookieJar, PrivateCookieJar, impl IntoResponse)> +{ + let (jar, current_station_cookie) = get_station_cookie(&db, jar).await; + let Some(station) = current_station_cookie else { + return Err((jar, pjar, Redirect::to("/s"))); + }; + + let content = html! { + nav { + ul { + li { strong { (format!("Station {}", station.name)) } } + } + ul { + li { a href="/s/station-logout" { "Station wechseln" } } + } + } + h1 { "Code eingeben" } + form action="/s/station-login" method="post" { + fieldset role="group" { + input type="text" name="pw" placeholder="Code" required; + input type="submit" value="Login"; + } + + } + }; + + Ok((jar, pjar, partials::page(content, session, false).await)) +} + +#[derive(Deserialize)] +struct LoginForm { + pw: String, +} + +async fn login( + State(db): State>, + session: Session, + jar: CookieJar, + pjar: PrivateCookieJar, + Form(form): Form, +) -> (CookieJar, PrivateCookieJar, impl IntoResponse) { + let (jar, current_station_cookie) = get_station_cookie(&db, jar).await; + let Some(station) = current_station_cookie else { + return (jar, pjar, Redirect::to("/s")); + }; + + let mut pjar = pjar; + + if form.pw == station.pw { + pjar = pjar.add(Cookie::new("pw", form.pw)); + succ!( + session, + "Erfolgreich eingeloggt, viel Spaß beim bewerten :-)" + ); + return (jar, pjar, Redirect::to(&format!("/s/{}", station.id))); + } else { + err!(session, "Falsches Passwort. Probiere es erneut."); + std::thread::sleep(std::time::Duration::from_secs(1)); + return (jar, pjar, Redirect::to("/s/code")); + } +} async fn logout(session: Session, mut jar: CookieJar) -> (CookieJar, impl IntoResponse) { jar = jar.remove(Cookie::from("station_id")); @@ -142,9 +228,40 @@ async fn logout(session: Session, mut jar: CookieJar) -> (CookieJar, impl IntoRe (jar, Redirect::to("/s")) } -pub(super) fn routes() -> Router> { +async fn quick_login( + State(db): State>, + session: Session, + jar: CookieJar, + pjar: PrivateCookieJar, + axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>, +) -> (CookieJar, PrivateCookieJar, impl IntoResponse) { + if let Some(station) = Station::find_by_id(&db, id).await { + if station.pw == code { + succ!( + session, + "Erfolgreich eingeloggt, viel Spaß beim bewerten :-)" + ); + let mut pjar = pjar.remove(Cookie::from("pw")); + let mut jar = jar.remove(Cookie::from("station_id")); + jar = jar.add(Cookie::new("station_id", id.to_string())); + pjar = pjar.add(Cookie::new("pw", code)); + + return (jar, pjar, Redirect::to(&format!("/s/{id}"))); + } + } + err!( + session, + "Falscher Quick-Einlogg-Link. Bitte nochmal scannen oder deine Station manuell auswählen:" + ); + return (jar, pjar, Redirect::to("/s")); +} + +pub(super) fn routes() -> Router { Router::new() .route("/", get(station_picker)) .route("/{id}", get(view)) + .route("/code", get(code)) + .route("/{id}/{code}", get(quick_login)) + .route("/station-login", post(login)) .route("/station-logout", get(logout)) }