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"
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"
@ -168,29 +133,6 @@ dependencies = [
"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]]
name = "backtrace"
version = "0.3.74"
@ -309,16 +251,6 @@ 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"
@ -340,11 +272,7 @@ 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",
]
@ -420,19 +348,9 @@ 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"
@ -705,16 +623,6 @@ 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"
@ -1080,15 +988,6 @@ 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"
@ -1324,12 +1223,6 @@ 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"
@ -1413,18 +1306,6 @@ 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"
@ -2090,7 +1971,6 @@ name = "stationslauf"
version = "0.1.0"
dependencies = [
"axum",
"axum-extra",
"chrono",
"dotenv",
"maud",
@ -2479,16 +2359,6 @@ 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"

View File

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

View File

@ -39,6 +39,17 @@ impl Station {
.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> {
sqlx::query!("INSERT INTO station(name) VALUES (?)", name)
.execute(db)

View File

@ -9,7 +9,7 @@ use partials::page;
use sqlx::SqlitePool;
use std::sync::Arc;
use tokio::net::TcpListener;
use tower_sessions::{cookie::Key, MemoryStore, SessionManagerLayer};
use tower_sessions::{MemoryStore, SessionManagerLayer};
pub(crate) mod admin;
mod partials;
@ -105,13 +105,6 @@ async fn serve_marker_png() -> Response<Body> {
#[derive(Clone)]
struct AppState {
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> {
@ -125,10 +118,7 @@ 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 state = AppState { db: Arc::new(db) };
let app = Router::new()
.nest("/s", station::routes()) // TODO: maybe switch to "/"

View File

@ -1,267 +1,97 @@
use crate::{err, partials, succ, AppState, Station};
use axum::{
extract::State,
response::{IntoResponse, Redirect},
routing::{get, post},
Form, Router,
};
use axum_extra::extract::{CookieJar, PrivateCookieJar};
use maud::{html, Markup, PreEscaped};
use serde::Deserialize;
use crate::{partials, AppState, Station};
use axum::{extract::State, routing::get, Router};
use maud::{html, Markup};
use sqlx::SqlitePool;
use std::sync::Arc;
use tower_sessions::{cookie::Cookie, Session};
use tower_sessions::Session;
async fn station_picker(
State(db): State<Arc<SqlitePool>>,
session: Session,
jar: CookieJar,
) -> Result<(CookieJar, Markup), (CookieJar, impl IntoResponse)> {
let (jar, current_station_cookie) = get_station_cookie(&db, jar).await;
if let Some(station) = current_station_cookie {
return Err((jar, Redirect::to(&format!("/s/{}", station.id))));
} else {
let stations = Station::all(&db).await;
let content = html! {
h1 { "Wähle deine Station" }
select onchange="window.location.href='/s/' + this.value;" {
option selected value="" {
"Deine Station..."
}
@for station in stations {
option value=(station.id) { (station.name) };
}
}
};
Ok((jar, partials::page(content, session, false).await))
}
}
async fn get_station_cookie(db: &SqlitePool, jar: CookieJar) -> (CookieJar, Option<Station>) {
let Some(station_id) = jar.get("station_id") else {
return (jar, None); // No station_id cookie
};
let station_id = match station_id.value().parse::<i64>() {
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 Some(station) = Station::find_by_id(db, station_id).await else {
// got some cookie with an i64 which is no valid station_id -> destroy and start over again
let jar = jar.remove(Cookie::from("station_id"));
return (jar, None);
};
(jar, Some(station))
}
async fn decide_between_stations(
cookie: &Station,
trying_to_access: &Station,
session: Session,
) -> Markup {
let content = html! {
h1 { "Wähle deine Station" }
ul {
li {
"Die neu aufgerufene "
(PreEscaped("&rarr; "))
a href=(format!("/s/{}", trying_to_access.id)) {
button { (trying_to_access.name) }
}
}
li {
"Die Alte "
(PreEscaped("&rarr; "))
a href=(format!("/s/{}", cookie.id)) {
button { (cookie.name) }
}
}
}
};
partials::page(content, session, false).await
}
//async fn view(
// State(db): State<Arc<SqlitePool>>,
// 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 view(
State(db): State<Arc<SqlitePool>>,
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)>,
) -> (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));
) -> Markup {
let Some(station) = Station::find_by_id_and_code(&db, id, &code).await else {
let content = html! {
article class="error" {
"Falscher Quick-Einlogg-Link. Bitte nochmal scannen oder neu eingeben."
}
};
return partials::page(content, session, false).await;
};
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"));
let content = html! {
h1 { (format!("Station {}", station.name)) }
};
partials::page(content, session, false).await
}
pub(super) fn routes() -> Router<AppState> {
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))
Router::new().route("/{id}/{code}", get(view))
}