weird login

This commit is contained in:
Philipp Hofer 2025-04-08 23:26:20 +02:00
parent 9c0e3e4fa7
commit 959f6c08df
11 changed files with 288 additions and 36 deletions

110
Cargo.lock generated
View File

@ -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"

View File

@ -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"] }

View File

@ -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<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index))
.nest("/station", station::routes())

View File

@ -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<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
web::routes()
}

View File

@ -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<Arc<SqlitePool>>, 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<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index))
.route("/", post(create))

View File

@ -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<String>,
amount_people: Option<i64>,
last_login: Option<NaiveDateTime>, // TODO use proper timestamp (NaiveDateTime?)
pw: String,
pub(crate) pw: String,
lat: Option<f64>,
lng: Option<f64>,
}
@ -139,6 +138,6 @@ impl Station {
}
}
pub(super) fn routes() -> Router<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
web::routes()
}

View File

@ -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<Arc<SqlitePool>>, 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<Arc<SqlitePool>>, session: Session) -> Markup {
page(content, session, false).await
}
pub(super) fn routes() -> Router<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index))
.route("/", post(create))

View File

@ -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<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
web::routes()
}

View File

@ -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<Arc<SqlitePool>>, 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<Arc<SqlitePool>>, session: Session) -> Markup {
page(content, session, false).await
}
pub(super) fn routes() -> Router<Arc<SqlitePool>> {
pub(super) fn routes() -> Router<AppState> {
Router::new()
.route("/", get(index))
.route("/", post(create))

View File

@ -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<Body> {
.unwrap()
}
#[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> {
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();

View File

@ -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<Arc<SqlitePool>>,
session: Session,
jar: CookieJar,
pjar: PrivateCookieJar,
axum::extract::Path(id): axum::extract::Path<i64>,
) -> 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(&current_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<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"));
@ -142,9 +228,40 @@ async fn logout(session: Session, mut jar: CookieJar) -> (CookieJar, impl IntoRe
(jar, Redirect::to("/s"))
}
pub(super) fn routes() -> Router<Arc<SqlitePool>> {
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));
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<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))
}