simply station login dramatically, only allow link 'login'

This commit is contained in:
Philipp Hofer 2025-04-11 12:47:39 +02:00
parent a69c5662e0
commit 982618b9a0
5 changed files with 97 additions and 397 deletions

130
Cargo.lock generated
View File

@ -17,41 +17,6 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 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]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "1.1.3" version = "1.1.3"
@ -168,29 +133,6 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum-extra"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45bf463831f5131b7d3c756525b305d40f1185b688565648a92e1392ca35713d"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http",
"http-body",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"serde",
"tower",
"tower-layer",
"tower-service",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.74" version = "0.3.74"
@ -309,16 +251,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -340,11 +272,7 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [ dependencies = [
"aes-gcm",
"base64",
"percent-encoding", "percent-encoding",
"rand",
"subtle",
"time", "time",
"version_check", "version_check",
] ]
@ -420,19 +348,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.9" version = "0.7.9"
@ -705,16 +623,6 @@ dependencies = [
"wasi 0.14.2+wasi-0.2.4", "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]] [[package]]
name = "gimli" name = "gimli"
version = "0.31.1" version = "0.31.1"
@ -1080,15 +988,6 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.11.0" version = "0.11.0"
@ -1324,12 +1223,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "parking" name = "parking"
version = "2.2.1" version = "2.2.1"
@ -1413,18 +1306,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 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]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@ -2090,7 +1971,6 @@ name = "stationslauf"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra",
"chrono", "chrono",
"dotenv", "dotenv",
"maud", "maud",
@ -2479,16 +2359,6 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" 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]] [[package]]
name = "unsafe-libyaml" name = "unsafe-libyaml"
version = "0.2.11" version = "0.2.11"

View File

@ -5,7 +5,6 @@ edition = "2024"
[dependencies] [dependencies]
axum = "0.8" axum = "0.8"
axum-extra = { version = "0.10", features = [ "cookie", "cookie-private" ]}
chrono = { version = "0.4", features = ["serde"]} chrono = { version = "0.4", features = ["serde"]}
dotenv = "0.15" dotenv = "0.15"
maud = { version = "0.27", features = ["axum"] } maud = { version = "0.27", features = ["axum"] }

View File

@ -39,6 +39,17 @@ impl Station {
.ok() .ok()
} }
pub async fn find_by_id_and_code(db: &SqlitePool, id: i64, code: &str) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, name, notes, amount_people, last_login, pw, lat, lng FROM station WHERE id = ? AND pw = ?",
id, code
)
.fetch_one(db)
.await
.ok()
}
async fn create(db: &SqlitePool, name: &str) -> Result<(), String> { async fn create(db: &SqlitePool, name: &str) -> Result<(), String> {
sqlx::query!("INSERT INTO station(name) VALUES (?)", name) sqlx::query!("INSERT INTO station(name) VALUES (?)", name)
.execute(db) .execute(db)

View File

@ -9,7 +9,7 @@ use partials::page;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::sync::Arc; use std::sync::Arc;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tower_sessions::{cookie::Key, MemoryStore, SessionManagerLayer}; use tower_sessions::{MemoryStore, SessionManagerLayer};
pub(crate) mod admin; pub(crate) mod admin;
mod partials; mod partials;
@ -105,13 +105,6 @@ async fn serve_marker_png() -> Response<Body> {
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
db: Arc<SqlitePool>, db: Arc<SqlitePool>,
key: Key,
}
impl FromRef<AppState> for Key {
fn from_ref(state: &AppState) -> Self {
state.key.clone()
}
} }
impl FromRef<AppState> for Arc<SqlitePool> { impl FromRef<AppState> for Arc<SqlitePool> {
@ -125,10 +118,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) {
let session_store = MemoryStore::default(); let session_store = MemoryStore::default();
let session_layer = SessionManagerLayer::new(session_store); let session_layer = SessionManagerLayer::new(session_store);
let state = AppState { let state = AppState { db: Arc::new(db) };
db: Arc::new(db),
key: Key::generate(),
};
let app = Router::new() let app = Router::new()
.nest("/s", station::routes()) // TODO: maybe switch to "/" .nest("/s", station::routes()) // TODO: maybe switch to "/"

View File

@ -1,267 +1,97 @@
use crate::{err, partials, succ, AppState, Station}; use crate::{partials, AppState, Station};
use axum::{ use axum::{extract::State, routing::get, Router};
extract::State, use maud::{html, Markup};
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use axum_extra::extract::{CookieJar, PrivateCookieJar};
use maud::{html, Markup, PreEscaped};
use serde::Deserialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use std::sync::Arc; use std::sync::Arc;
use tower_sessions::{cookie::Cookie, Session}; use tower_sessions::Session;
async fn station_picker( //async fn view(
State(db): State<Arc<SqlitePool>>, // State(db): State<Arc<SqlitePool>>,
session: Session, // session: Session,
jar: CookieJar, // jar: CookieJar,
) -> Result<(CookieJar, Markup), (CookieJar, impl IntoResponse)> { // pjar: PrivateCookieJar,
let (jar, current_station_cookie) = get_station_cookie(&db, jar).await; // axum::extract::Path(id): axum::extract::Path<i64>,
if let Some(station) = current_station_cookie { //) -> Result<(CookieJar, PrivateCookieJar, Markup), (CookieJar, PrivateCookieJar, impl IntoResponse)>
return Err((jar, Redirect::to(&format!("/s/{}", station.id)))); //{
} else { // // Station selector
let stations = Station::all(&db).await; // let (mut jar, current_station_cookie) = get_station_cookie(&db, jar).await;
let content = html! { // if current_station_cookie.is_none() {
h1 { "Wähle deine Station" } // jar = jar.add(Cookie::new("station_id", id.to_string()));
select onchange="window.location.href='/s/' + this.value;" { // }
option selected value="" { // if let Some(current_station_cookie) = current_station_cookie {
"Deine Station..." // if current_station_cookie.id != id {
} // // user has a cookie, which is a different station than she is trying to access
@for station in stations { // if let Some(station) = Station::find_by_id(&db, id).await {
option value=(station.id) { (station.name) }; // jar = jar.remove(Cookie::from("station_id"));
} // // trying to access valid station id
} // 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,
Ok((jar, partials::page(content, session, false).await)) // pjar,
} // decide_between_stations(&current_station_cookie, &station, session).await,
} // ));
// } else {
async fn get_station_cookie(db: &SqlitePool, jar: CookieJar) -> (CookieJar, Option<Station>) { // // user trying to access _in_valid station id -> make her aware + redirect to old
let Some(station_id) = jar.get("station_id") else { // 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 (jar, None); // No station_id cookie // return Err((
}; // jar,
// pjar,
let station_id = match station_id.value().parse::<i64>() { // Redirect::to(&format!("/s/{}", current_station_cookie.id)),
Ok(number) => number, // ));
Err(_) => { // }
// got some cookie which isn't a i64 -> destroy and start over again // }
let jar = jar.remove(Cookie::from("station_id")); // }
return (jar, None); // let station = Station::find_by_id(&db, id).await.unwrap();
} //
}; // let mut pjar = pjar;
//
let Some(station) = Station::find_by_id(db, station_id).await else { // // PW Checker
// got some cookie with an i64 which is no valid station_id -> destroy and start over again // if let Some(pw) = pjar.get("pw") {
let jar = jar.remove(Cookie::from("station_id")); // if pw.value() != station.pw {
return (jar, None); // 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",))));
(jar, Some(station)) // }
} // } else {
// return Err((jar, pjar, Redirect::to(&format!("/s/code",))));
async fn decide_between_stations( // }
cookie: &Station, //
trying_to_access: &Station, // let content = html! {
session: Session, // nav {
) -> Markup { // ul {
let content = html! { // li { strong { (format!("Station {}", station.name)) } }
h1 { "Wähle deine Station" } // }
ul { // ul {
li { // li { a href="/s/station-logout" { "Logout" } }
"Die neu aufgerufene " // }
(PreEscaped("&rarr; ")) // }
a href=(format!("/s/{}", trying_to_access.id)) { // h1 { "test" }
button { (trying_to_access.name) } // };
} //
} // Ok((jar, pjar, partials::page(content, session, false).await))
li { //}
"Die Alte "
(PreEscaped("&rarr; "))
a href=(format!("/s/{}", cookie.id)) {
button { (cookie.name) }
}
}
}
};
partials::page(content, session, false).await
}
async fn view( async fn view(
State(db): State<Arc<SqlitePool>>, State(db): State<Arc<SqlitePool>>,
session: Session, session: Session,
jar: CookieJar,
pjar: PrivateCookieJar,
axum::extract::Path(id): axum::extract::Path<i64>,
) -> 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()));
}
if let Some(current_station_cookie) = current_station_cookie {
if current_station_cookie.id != id {
// user has a cookie, which is a different station than she is trying to access
if let Some(station) = Station::find_by_id(&db, id).await {
jar = jar.remove(Cookie::from("station_id"));
// trying to access valid station id
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(&current_station_cookie, &station, session).await,
));
} else {
// user trying to access _in_valid station id -> make her aware + redirect to old
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 {
li { strong { (format!("Station {}", station.name)) } }
}
ul {
li { a href="/s/station-logout" { "Logout" } }
}
}
h1 { "test" }
};
Ok((jar, pjar, partials::page(content, session, false).await))
}
async fn code(
State(db): State<Arc<SqlitePool>>,
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<Arc<SqlitePool>>,
session: Session,
jar: CookieJar,
pjar: PrivateCookieJar,
Form(form): Form<LoginForm>,
) -> (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"));
succ!(session, "Erfolgreich ausgeloggt!");
(jar, Redirect::to("/s"))
}
async fn quick_login(
State(db): State<Arc<SqlitePool>>,
session: Session,
jar: CookieJar,
pjar: PrivateCookieJar,
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>, axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
) -> (CookieJar, PrivateCookieJar, impl IntoResponse) { ) -> Markup {
if let Some(station) = Station::find_by_id(&db, id).await { let Some(station) = Station::find_by_id_and_code(&db, id, &code).await else {
if station.pw == code { let content = html! {
succ!( article class="error" {
session, "Falscher Quick-Einlogg-Link. Bitte nochmal scannen oder neu eingeben."
"Erfolgreich eingeloggt, viel Spaß beim bewerten :-)" }
); };
let mut pjar = pjar.remove(Cookie::from("pw")); return partials::page(content, session, false).await;
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}"))); let content = html! {
} h1 { (format!("Station {}", station.name)) }
} };
err!(
session, partials::page(content, session, false).await
"Falscher Quick-Einlogg-Link. Bitte nochmal scannen oder deine Station manuell auswählen:"
);
return (jar, pjar, Redirect::to("/s"));
} }
pub(super) fn routes() -> Router<AppState> { pub(super) fn routes() -> Router<AppState> {
Router::new() Router::new().route("/{id}/{code}", get(view))
.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))
} }