feat: add logic for journey->train identifier resolving, add recording of journeys, build api bearer auth
This commit is contained in:
2159
Cargo.lock
generated
2159
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@ -4,4 +4,22 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[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
9
config.yaml
Normal 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
|
71
src/api/db_vendo_navigator.rs
Normal file
71
src/api/db_vendo_navigator.rs
Normal 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 => ""
|
||||||
|
}
|
||||||
|
}
|
3
src/api/handlers/bingo_card.rs
Normal file
3
src/api/handlers/bingo_card.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub async fn get() -> impl axum::response::IntoResponse {
|
||||||
|
todo!();
|
||||||
|
}
|
2
src/api/handlers/mod.rs
Normal file
2
src/api/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) mod bingo_card;
|
||||||
|
pub(crate) mod webhook;
|
103
src/api/handlers/webhook.rs
Normal file
103
src/api/handlers/webhook.rs
Normal 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
2
src/api/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub(crate) mod handlers;
|
||||||
|
mod db_vendo_navigator;
|
50
src/config.rs
Normal file
50
src/config.rs
Normal 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
31
src/database.rs
Normal 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
1
src/error/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub(crate) mod train_order_api_error;
|
81
src/error/train_order_api_error.rs
Normal file
81
src/error/train_order_api_error.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
48
src/main.rs
48
src/main.rs
@ -1,3 +1,47 @@
|
|||||||
fn main() {
|
use std::net::SocketAddr;
|
||||||
println!("Hello, world!");
|
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
43
src/model/app.rs
Normal 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
6
src/model/bingo_card.rs
Normal 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
14
src/model/database.rs
Normal 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>,
|
||||||
|
}
|
26
src/model/db_vendo_navigator_api.rs
Normal file
26
src/model/db_vendo_navigator_api.rs
Normal 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
8
src/model/mod.rs
Normal 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
6
src/model/traewelling.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct CheckIn {
|
||||||
|
|
||||||
|
}
|
4
src/model/train.rs
Normal file
4
src/model/train.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
use crate::model::uic::UIC;
|
||||||
|
pub(crate) struct Train {
|
||||||
|
uic: UIC,
|
||||||
|
}
|
92
src/model/travelynx.rs
Normal file
92
src/model/travelynx.rs
Normal 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
98
src/model/uic.rs
Normal 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],
|
||||||
|
}
|
@ -1,41 +1,44 @@
|
|||||||
CREATE TABLE IF NOT EXISTS triebzug (
|
CREATE TABLE IF NOT EXISTS triebzug (
|
||||||
id INT PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
tz_id INT UNIQUE,
|
tz_id INT NOT NULL UNIQUE,
|
||||||
name VARCHAR NULL
|
name VARCHAR NULL,
|
||||||
);
|
UNIQUE (uuid)
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS bingo_cards (
|
CREATE TABLE IF NOT EXISTS bingo_cards (
|
||||||
id INT PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
card_id INT NOT NULL REFERENCES bingo_card(id),
|
|
||||||
start_time INT NOT NULL,
|
start_time INT NOT NULL,
|
||||||
end_time INT NOT NULL
|
end_time INT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS bingo_card (
|
||||||
id INT PRIMARY KEY,
|
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
|
name VARCHAR NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS webhook (
|
CREATE TABLE IF NOT EXISTS webhook (
|
||||||
id INT PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
user INT NOT NULL REFERENCES user(id),
|
"user" UUID NOT NULL REFERENCES "user" (uuid),
|
||||||
secret VARCHAR NOT NULL,
|
token VARCHAR NOT NULL
|
||||||
hook VARCHAR NOT NULL
|
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS checkins (
|
CREATE TABLE IF NOT EXISTS checkins (
|
||||||
id INT PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
user INT NOT NULL REFERENCES user(id),
|
"user" UUID NOT NULL REFERENCES "user" (uuid),
|
||||||
|
train UUID NOT NULL REFERENCES triebzug(uuid),
|
||||||
start_time TIMESTAMP NOT NULL,
|
start_time TIMESTAMP NOT NULL,
|
||||||
end_time TIMESTAMP NULL
|
end_time TIMESTAMP NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX tz_id ON triebzug(tz_id);
|
CREATE INDEX tz_id ON triebzug(tz_id);
|
||||||
CREATE INDEX bingo_card_xy ON bingo_card(x_pos, y_pos);
|
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
37
src/util/axum.rs
Normal 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
1
src/util/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod axum;
|
Reference in New Issue
Block a user