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"
|
||||
|
||||
[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() {
|
||||
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
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 (
|
||||
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
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