feat: add logic for journey->train identifier resolving, add recording of journeys, build api bearer auth

This commit is contained in:
2025-05-11 01:12:23 +02:00
parent 6b389e2d95
commit 41651dac21
25 changed files with 2833 additions and 125 deletions

2159
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -4,4 +4,22 @@ version = "0.1.0"
edition = "2021"
[dependencies]
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio-rustls", "any"] }
sqlx = { version = "0.8.5", features = ["postgres", "runtime-tokio-rustls", "any", "macros", "uuid", "chrono"] }
uuid = { version = "1.16.0", features = ["v4"] }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.140"
url = { version = "2.5.2", features = ["serde"] }
urlencoding = "2.1.3"
config = "0.15.11"
openidconnect = "3.5.0"
tokio = { version = "1.45.0", features = ["full"] }
axum = { version = "0.8.4", features = ["tracing", "multipart", "macros", "http2"] }
axum-core = { version = "0.5.2", features = ["tracing"] }
axum-extra = { version = "0.10.1", features = ["cookie", "form"] }
axum-auth = { version = "0.8.1", features = ["auth-bearer"] }
reqwest = { version = "0.12.15", features = ["json"] }
strum = { version = "0.27.1", features = ["derive"] }
strum_macros = "0.27.1"
chrono = { version = "0.4.41", features = ["alloc"] }
async-trait = "0.1.88"

9
config.yaml Normal file
View File

@ -0,0 +1,9 @@
http:
bind_address: 127.0.0.1
port: 3002
database:
type: postgresql
host: 127.0.0.1
user: postgres
password: mysecretpassword
database: postgres

View File

@ -0,0 +1,71 @@
use chrono::DateTime;
use url::Url;
use urlencoding::encode;
use crate::error::train_order_api_error::ResolveTripNumberError;
use crate::model::db_vendo_navigator_api::TrainOrdering;
use crate::model::travelynx::TrainType;
pub async fn get_railcar_identifier_by_journey(
train_type: TrainType,
trip_number: usize,
station_uic: usize,
departure_time: usize
) -> Result<u64, ResolveTripNumberError> {
let train_ordering = query_train_order_api(train_type, trip_number, station_uic, departure_time).await?;
println!("Received train ordering response {:?}", train_ordering);
find_railcar_identifier(train_ordering)
}
fn find_railcar_identifier(train_ordering: TrainOrdering) -> Result<u64, ResolveTripNumberError> {
let trainset = train_ordering.train_sets.first()
.ok_or(ResolveTripNumberError::Api("No items in field 'fahrzeuggruppe'".to_string()))?;
let identifier_str = trainset.identifier.to_owned();
let train_type = &trainset.journey.train_type;
let identifier = crop_first_n_chars(identifier_str.as_str(), train_type.to_string().len())
.to_string().parse::<u64>()?;
Ok(identifier)
}
async fn query_train_order_api(
train_type: TrainType,
trip_number: usize,
station_uic: usize,
departure_time: usize
) -> reqwest::Result<TrainOrdering> {
println!("Resolving trip {train_type} {trip_number} from {station_uic} at {departure_time}");
let client = reqwest::ClientBuilder::new()
.build()?;
let api_url = build_api_url(train_type, trip_number, station_uic, departure_time);
println!("Fetching {api_url}");
client.get(api_url)
.header("Accept", "application/x.db.vendo.mob.wagenreihung.v3+json")
.header("X-Correlation-ID", "ABCDE")
.send()
.await?
.json::<TrainOrdering>()
.await
}
fn build_api_url(
train_type: TrainType,
trip_number: usize,
station_uic: usize,
departure_time: usize
) -> Url {
let train_trip = format!("{}_{}", train_type.to_string(), trip_number);
let departure = DateTime::from_timestamp(departure_time as i64, 0)
.expect("invalid departure time");
Url::parse(format!(
"https://app.vendo.noncd.db.de/mob/zuglaeufe/{}/halte/by-abfahrt/{}_{}/wagenreihung",
train_trip,
station_uic,
encode(departure.to_rfc3339().as_str())
).as_str()).unwrap()
}
fn crop_first_n_chars(s: &str, count: usize) -> &str {
match s.char_indices().skip(count).next() {
Some((pos, _)) => &s[pos..],
None => ""
}
}

View File

@ -0,0 +1,3 @@
pub async fn get() -> impl axum::response::IntoResponse {
todo!();
}

2
src/api/handlers/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub(crate) mod bingo_card;
pub(crate) mod webhook;

103
src/api/handlers/webhook.rs Normal file
View File

@ -0,0 +1,103 @@
use std::sync::Arc;
use axum::{
Json,
http::StatusCode,
extract::State,
};
use axum_core::response::IntoResponse;
use chrono::DateTime;
use serde::Deserialize;
use sqlx::PgPool;
use sqlx::postgres::PgQueryResult;
use crate::api::db_vendo_navigator::get_railcar_identifier_by_journey;
use crate::error::train_order_api_error::{CheckInError, ResolveTripNumberError};
use crate::model::app::AppState;
use crate::model::database::User;
use crate::model::travelynx::{CheckInReason, Train};
use crate::util::axum::UserBearerTokenExtractor;
#[derive(Debug, Deserialize)]
pub(crate) enum CheckIn {
Traewelling(crate::model::traewelling::CheckIn),
Travelynx(crate::model::travelynx::CheckIn),
}
#[axum::debug_handler]
pub(crate) async fn receive_travelynx(
State(app_state): State<AppState>,
UserBearerTokenExtractor(user): UserBearerTokenExtractor,
Json(body): Json<crate::model::travelynx::CheckIn>,
) -> impl IntoResponse {
receive_travelynx_checkin(body, user, app_state.db).await.into_response()
}
async fn receive_travelynx_checkin(
check_in: crate::model::travelynx::CheckIn,
user: User,
db: Arc<PgPool>
) -> impl IntoResponse {
match check_in.reason {
CheckInReason::CHECKIN => {
let railcar_identifier = get_railcar_identifier_by_journey(
check_in.status.train.train_type.unwrap(),
check_in.status.train.number.to_owned().unwrap().parse().unwrap(),
check_in.status.from_station.uic,
check_in.status.from_station.scheduled_time.unwrap()
)
.await
.map_err(|e| CheckInError::from(e))?;
let train = get_train_by_identifier(railcar_identifier as i32, db.clone())
.await
.map_err(|e| ResolveTripNumberError::from(e))
.map_err(|e| CheckInError::from(e))?;
println!("Train: {:?}", train);
record_journey(user, train, check_in.status.from_station.scheduled_time.unwrap(), db)
.await
.expect("Failed to check in!");
let message = format!(
"Successfully checked into {} {} ({}), departing from station {} at {}",
check_in.status.train.train_type.unwrap(),
check_in.status.train.number.unwrap(),
railcar_identifier,
check_in.status.from_station.uic,
check_in.status.from_station.scheduled_time.unwrap()
);
Ok::<_, CheckInError>((
StatusCode::OK,
message
).into_response())
},
CheckInReason::UNDO => {
Ok::<_, CheckInError>((
StatusCode::OK,
"Checkin undone"
).into_response())
},
_ => Ok::<_, CheckInError>((
StatusCode::OK,
"Nothing to do!"
).into_response())
}
}
async fn get_train_by_identifier(identifier: i32, db: Arc<PgPool>) -> sqlx::Result<crate::model::database::Train> {
sqlx::query_as!(
crate::model::database::Train,
"SELECT * FROM triebzug WHERE tz_id = $1",
identifier
).fetch_one(db.as_ref()).await
}
async fn record_journey(
user: User,
train: crate::model::database::Train,
timestamp: usize,
db: Arc<PgPool>
) -> sqlx::Result<PgQueryResult> {
sqlx::query!(
"INSERT INTO checkins VALUES (gen_random_uuid(), $1, $2, $3, NULL)",
user.uuid,
train.uuid,
DateTime::from_timestamp(timestamp as i64, 0).unwrap().naive_utc()
).execute(db.as_ref()).await
}

2
src/api/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub(crate) mod handlers;
mod db_vendo_navigator;

50
src/config.rs Normal file
View File

@ -0,0 +1,50 @@
use std::net::IpAddr;
use serde::Deserialize;
use url::Url;
use config::{
Config,
ConfigError,
File,
};
#[derive(Debug, Clone, Deserialize)]
pub struct IceBingoConfig {
pub http: HttpServerConfig,
//pub oidc: OidcConfig,
pub database: DatabaseConfig,
}
#[derive(Debug, Clone, Deserialize)]
pub struct HttpServerConfig {
pub bind_address: IpAddr,
pub port: u16,
}
#[derive(Debug, Clone, Deserialize)]
pub struct OidcConfig {
pub issuer: Url,
pub client_id: String,
pub client_secret: String,
pub callback: Url,
pub introspection_url: Url,
}
#[derive(Debug, Clone, Deserialize)]
pub struct DatabaseConfig {
#[serde(rename = "type")]
pub db_type: String,
pub host: String,
pub user: String,
pub password: String,
pub database: String,
}
impl IceBingoConfig {
pub async fn load(filename: &str) -> Result<Self, ConfigError> {
Config::builder()
.add_source(File::with_name(filename))
.build()?
.try_deserialize()
}
}

31
src/database.rs Normal file
View File

@ -0,0 +1,31 @@
use std::sync::Arc;
use sqlx::PgPool;
use crate::model::database::User;
#[derive(Debug)]
pub(crate) enum UserNotFoundError {
Uuid(uuid::Error),
Sqlx(sqlx::Error),
NotFound(String),
}
impl From<uuid::Error> for UserNotFoundError {
fn from(e: uuid::Error) -> Self { UserNotFoundError::Uuid(e)}
}
impl From<sqlx::Error> for UserNotFoundError {
fn from(e: sqlx::Error) -> Self { UserNotFoundError::Sqlx(e) }
}
pub(crate) async fn get_user_by_token(
token: String,
db: Arc<PgPool>
) -> Result<User, UserNotFoundError> {
sqlx::query_as!(
User,
"SELECT u.* FROM \"user\" u JOIN webhook w ON w.user = u.uuid WHERE w.token = $1",
token
).fetch_one(db.as_ref())
.await
.map_err(|err| err.into())
}

1
src/error/mod.rs Normal file
View File

@ -0,0 +1 @@
pub(crate) mod train_order_api_error;

View File

@ -0,0 +1,81 @@
use std::fmt::{Display, Formatter};
use axum_core::response::IntoResponse;
use reqwest::StatusCode;
#[derive(Debug)]
pub enum ResolveTripNumberError {
Reqwest(reqwest::Error),
Serde(serde_json::Error),
Io(std::io::Error),
Api(String),
ParseIntError(std::num::ParseIntError),
Sqlx(sqlx::Error),
}
impl Display for ResolveTripNumberError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
ResolveTripNumberError::Reqwest(e) => f.write_fmt(format_args!("Reqwest error on url={:?}: {:?} (status: {:?})", e.url(), e, e.status())),
ResolveTripNumberError::Serde(e) => f.write_fmt(format_args!("Serde error: {:?}", e)),
ResolveTripNumberError::Io(e) => f.write_fmt(format_args!("Io error: {:?}", e)),
ResolveTripNumberError::Api(e) => f.write_fmt(format_args!("Api error: {:?}", e)),
ResolveTripNumberError::ParseIntError(e) => f.write_fmt(format_args!("Parse int error: {:?}", e)),
ResolveTripNumberError::Sqlx(e) => f.write_fmt(format_args!("Sqlx error: {:?}", e)),
}
}
}
impl std::error::Error for ResolveTripNumberError {}
impl From<reqwest::Error> for ResolveTripNumberError {
fn from(err: reqwest::Error) -> Self { ResolveTripNumberError::Reqwest(err) }
}
impl From<serde_json::Error> for ResolveTripNumberError {
fn from(err: serde_json::Error) -> Self { ResolveTripNumberError::Serde(err) }
}
impl From<std::io::Error> for ResolveTripNumberError {
fn from(err: std::io::Error) -> Self { ResolveTripNumberError::Io(err) }
}
impl From<std::num::ParseIntError> for ResolveTripNumberError {
fn from(err: std::num::ParseIntError) -> Self { ResolveTripNumberError::ParseIntError(err) }
}
impl From<String> for ResolveTripNumberError {
fn from(err: String) -> Self { ResolveTripNumberError::Api(err) }
}
impl From<sqlx::Error> for ResolveTripNumberError {
fn from(err: sqlx::Error) -> Self { ResolveTripNumberError::Sqlx(err) }
}
#[derive(Debug)]
pub enum CheckInError {
ResolveError(ResolveTripNumberError),
}
impl From<ResolveTripNumberError> for CheckInError {
fn from(err: ResolveTripNumberError) -> Self { CheckInError::ResolveError(err) }
}
impl Display for CheckInError {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
match self {
CheckInError::ResolveError(e) => f.write_str(e.to_string().as_str()),
}
}
}
pub type ApiResult<T> = Result<T, CheckInError>;
impl IntoResponse for CheckInError {
fn into_response(self) -> axum_core::response::Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Error processing checkin: {}", self)
).into_response()
}
}

View File

@ -1,3 +1,47 @@
fn main() {
println!("Hello, world!");
use std::net::SocketAddr;
use axum::Router;
use axum::routing::{get, post};
use sqlx::migrate::MigrateError;
use tokio::net::TcpListener;
use crate::config::IceBingoConfig;
use crate::model::app::AppState;
mod config;
pub(crate) mod model;
mod api;
pub(crate) mod error;
pub(crate) mod util;
mod database;
#[tokio::main]
async fn main() {
println!("Starting up");
let config = IceBingoConfig::load("config.yaml")
.await.expect("Unable to load config");
let state: AppState = AppState::new(config.clone()).await.unwrap();
//migrate(state.clone()).await.expect("Database migration failed");
let router = build_router(state);
let listener = listen(config).await;
axum::serve::serve(listener, router).await.unwrap()
}
fn build_router(state: AppState) -> Router {
Router::new()
.route("/webhook/travelynx", post(api::handlers::webhook::receive_travelynx))
.route("/card/", get(api::handlers::bingo_card::get))
.with_state(state)
}
async fn listen(config: IceBingoConfig) -> TcpListener {
let socket_addr: SocketAddr = (config.http.bind_address, config.http.port).into();
println!("Listening on {}", socket_addr);
TcpListener::bind(socket_addr).await.unwrap()
}
async fn migrate(state: AppState) -> Result<(), MigrateError>{
sqlx::migrate!("./src/sql/migrations")
.run(&mut state.db.acquire().await?)
.await
}

43
src/model/app.rs Normal file
View File

@ -0,0 +1,43 @@
use std::collections::HashMap;
use std::sync::Arc;
use sqlx::{PgPool};
use sqlx::pool::PoolOptions;
use tokio::sync::Mutex;
use crate::config::IceBingoConfig;
#[derive(Clone)]
pub(crate) struct AppState
{
pub state: Arc<Mutex<HashMap<String, String>>>,
pub db: Arc<PgPool>,
}
impl AppState {
pub(crate) async fn new(config: IceBingoConfig) -> Result<Self, sqlx::Error> {
let kv_store: HashMap<String, String> = HashMap::new();
let state_mutex = Arc::new(Mutex::new(kv_store));
let db_url = get_db_url(&config);
let pool_options = PoolOptions::new()
.max_connections(20)
.min_connections(5);
let pool = pool_options.connect(db_url.as_str()).await?;
Ok(Self {
state: state_mutex,
db: Arc::new(pool)
})
}
}
fn get_db_url(config: &IceBingoConfig) -> String {
match config.database.db_type.as_str() {
"postgresql" => format!(
"postgresql://{}:{}@{}/{}",
config.database.user,
config.database.password,
config.database.host,
config.database.database
),
_ => "".to_string()
}
}

6
src/model/bingo_card.rs Normal file
View File

@ -0,0 +1,6 @@
use crate::model::train::Train;
struct BingoCard {
id: uuid::Uuid,
fields: [Train; 24],
}

14
src/model/database.rs Normal file
View File

@ -0,0 +1,14 @@
use uuid::Uuid;
#[derive(sqlx::FromRow, Debug, Clone)]
pub struct User {
pub uuid: Uuid,
pub name: String,
}
#[derive(sqlx::FromRow, Debug, Clone)]
pub struct Train {
pub uuid: Uuid,
pub tz_id: i32,
pub name: Option<String>,
}

View File

@ -0,0 +1,26 @@
use serde::Deserialize;
use crate::model::travelynx::TrainType;
#[derive(Debug, Deserialize)]
pub struct TrainOrdering {
#[serde(rename = "fahrzeuggruppen")]
pub train_sets: Vec<TrainSet>,
}
#[derive(Debug, Deserialize)]
pub struct TrainSet {
#[serde(rename = "bezeichnung")]
pub identifier: String,
#[serde(rename = "fahrtreferenz")]
pub journey: Journey,
}
#[derive(Debug, Deserialize)]
pub struct Journey {
#[serde(rename = "typ")]
pub category: String,
#[serde(rename = "gattung")]
pub train_type: TrainType,
#[serde(rename = "fahrtnummer")]
pub trip_number: usize,
}

8
src/model/mod.rs Normal file
View File

@ -0,0 +1,8 @@
pub mod uic;
pub mod db_vendo_navigator_api;
pub(crate) mod travelynx;
pub(crate) mod traewelling;
mod bingo_card;
mod train;
pub(crate) mod app;
pub(crate) mod database;

6
src/model/traewelling.rs Normal file
View File

@ -0,0 +1,6 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub(crate) struct CheckIn {
}

4
src/model/train.rs Normal file
View File

@ -0,0 +1,4 @@
use crate::model::uic::UIC;
pub(crate) struct Train {
uic: UIC,
}

92
src/model/travelynx.rs Normal file
View File

@ -0,0 +1,92 @@
use serde::{de, Deserialize};
use strum_macros::{Display, EnumString};
#[derive(Debug, Deserialize)]
pub(crate) struct CheckIn {
#[serde(deserialize_with = "reason_to_enum")]
pub reason: CheckInReason,
pub status: CheckInStatus,
}
#[derive(Debug, Deserialize, EnumString)]
#[strum(ascii_case_insensitive)]
pub enum CheckInReason {
PING,
CHECKIN,
UPDATE,
UNDO,
}
fn reason_to_enum<'de, D>(deserializer: D) -> Result<CheckInReason, D::Error>
where
D: de::Deserializer<'de>,
{
let s: &str = de::Deserialize::deserialize(deserializer)?;
CheckInReason::try_from(s).map_err(de::Error::custom)
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckInStatus {
action_time: usize,
backend: Backend,
checked_in: bool,
comment: Option<String>,
deprecated: bool,
pub from_station: Station,
//pub to_station: Station,
pub train: Train
}
#[derive(Debug, Deserialize, EnumString)]
#[strum(ascii_case_insensitive)]
enum BackendType {
DBRIS,
}
#[derive(Debug, Deserialize)]
struct Backend {
id: u8,
name: String,
#[serde(rename = "type")]
backend_type: BackendType,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Station {
ds100: Option<String>,
latitude: Option<f64>,
longitude: Option<f64>,
platform: Option<String>,
real_time: Option<usize>,
pub scheduled_time: Option<usize>,
pub uic: usize,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Train {
hafas_id: Option<String>,
id: String,
line: Option<String>,
#[serde(rename = "no")]
pub number: Option<String>,
#[serde(rename = "type")]
pub train_type: Option<TrainType>,
}
#[derive(Debug, Display, Deserialize, EnumString, Copy, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub enum TrainType {
ICE,
IC,
ECE,
EC,
RE,
RB,
MEX,
S,
U,
BUS,
}

98
src/model/uic.rs Normal file
View File

@ -0,0 +1,98 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct UIC {
type_code: TypeCode,
country_code: CountryCode,
national_block: NationalBlock,
check: char,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum TypeCode {
Miscellaneous = 90,
ElectricLocomotive = 91,
DieselLocomotive = 92,
ElectricMultipleUnitHighSpeed = 93,
ElectricMultipleUnitLowSpeed = 94,
DieselMultipleUnit = 95,
SpecialisedTrailer = 96,
ElectricShunter = 97,
DieselShunter = 98,
SpecialVehicle = 99,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
enum CountryCode {
Finland = 10,
Russia = 20,
Belarus = 21,
Ukraine = 22,
Moldova = 23,
Lithuania = 24,
Latvia = 25,
Estonia = 26,
Kazakhstan = 27,
Georgia = 28,
Uzbekistan = 29,
NorthKorea = 30,
Mongolia = 31,
Vietnam = 32,
China = 33,
Laos = 34,
Cuba = 40,
Albania = 41,
Japan = 42,
SerbRepublicOfBosniaHerzegovina = 44,
BosniaHerzegovina = 49,
MuslimCroatFederationOfBosniaHerzegovina = 50,
Poland = 51,
Bulgaria = 52,
Romania = 53,
CzechRepublic = 54,
Hungary = 55,
Slovakia = 56,
Azerbaijan = 57,
Armenia = 58,
Kyrgyzstan = 59,
Ireland = 60,
SouthKorea = 61,
Montenegro = 62,
NorthMacedonia = 65,
Tajikistan = 66,
Turkmenistan = 67,
Afghanistan = 68,
UnitedKingdom = 70,
Spain = 71,
Serbia = 72,
GreeceKingdom = 73,
Sweden = 74,
Turkey = 75,
Norway = 76,
Croatia = 78,
Slovenia = 79,
Germany = 80,
Austria = 81,
Luxembourg = 82,
Italian = 83,
Netherlands = 84,
Switzerland = 85,
Denmark = 86,
France = 87,
Belgium = 88,
Tanzania = 89,
Egypt = 90,
Tunesia = 91,
Algeria = 92,
Marocco = 93,
Portugal = 94,
Israel = 95,
Iran = 96,
Syriac = 97,
Lebanon = 98,
Iraq = 99,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct NationalBlock {
digits: [char; 7],
}

View File

@ -1,41 +1,44 @@
CREATE TABLE IF NOT EXISTS triebzug (
id INT PRIMARY KEY,
tz_id INT UNIQUE,
name VARCHAR NULL
);
CREATE TABLE IF NOT EXISTS bingo_card (
id INT PRIMARY KEY,
x_pos INT NOT NULL,
y_pos INT NOT NULL,
tz_id INT NOT NULL REFERENCES triebzug(tz_id)
uuid UUID PRIMARY KEY,
tz_id INT NOT NULL UNIQUE,
name VARCHAR NULL,
UNIQUE (uuid)
);
CREATE TABLE IF NOT EXISTS bingo_cards (
id INT PRIMARY KEY,
card_id INT NOT NULL REFERENCES bingo_card(id),
uuid UUID PRIMARY KEY,
start_time INT NOT NULL,
end_time INT NOT NULL
);
CREATE TABLE IF NOT EXISTS user (
id INT PRIMARY KEY,
CREATE TABLE IF NOT EXISTS bingo_card (
uuid UUID NOT NULL,
x_pos INT NOT NULL,
y_pos INT NOT NULL,
triebzug UUID NOT NULL REFERENCES triebzug(uuid),
card_uuid UUID NOT NULL REFERENCES bingo_cards(uuid),
PRIMARY KEY (uuid, x_pos, y_pos)
);
CREATE TABLE IF NOT EXISTS "user" (
uuid UUID PRIMARY KEY,
name VARCHAR NOT NULL
);
CREATE TABLE IF NOT EXISTS webhook (
id INT PRIMARY KEY,
user INT NOT NULL REFERENCES user(id),
secret VARCHAR NOT NULL,
hook VARCHAR NOT NULL
uuid UUID PRIMARY KEY,
"user" UUID NOT NULL REFERENCES "user" (uuid),
token VARCHAR NOT NULL
);
CREATE TABLE IF NOT EXISTS checkins (
id INT PRIMARY KEY,
user INT NOT NULL REFERENCES user(id),
uuid UUID PRIMARY KEY,
"user" UUID NOT NULL REFERENCES "user" (uuid),
train UUID NOT NULL REFERENCES triebzug(uuid),
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NULL
);
CREATE INDEX tz_id ON triebzug(tz_id);
CREATE INDEX bingo_card_xy ON bingo_card(x_pos, y_pos);
CREATE INDEX bingo_card_uuid ON bingo_card(uuid);

37
src/util/axum.rs Normal file
View File

@ -0,0 +1,37 @@
use axum::http::request::Parts;
use axum::http::StatusCode;
use axum::extract::FromRequestParts;
use axum_auth::{AuthBearer, Rejection};
use axum_core::extract::FromRef;
use crate::database::{get_user_by_token, UserNotFoundError};
use crate::model::app::AppState;
use crate::model::database::User;
pub(crate) struct UserBearerTokenExtractor(pub User);
pub(crate) type InvalidBearerTokenRejection = Rejection;
impl From<UserNotFoundError> for InvalidBearerTokenRejection {
fn from(e: UserNotFoundError) -> Self {
(
StatusCode::UNAUTHORIZED,
"No user associated with this bearer token"
).into()
}
}
impl<P> FromRequestParts<P> for UserBearerTokenExtractor
where
AppState: FromRef<P>,
P: Send + Sync,
{
type Rejection = InvalidBearerTokenRejection;
async fn from_request_parts(parts: &mut Parts, state: &P) -> Result<Self, Self::Rejection>
{
let AuthBearer(bearer_token) = AuthBearer::from_request_parts(parts, state)
.await?;
let state = AppState::from_ref(state);
let user = get_user_by_token(bearer_token, state.db).await?;
Ok(UserBearerTokenExtractor(user))
}
}

1
src/util/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod axum;