Harden Rust backend deployment
This commit is contained in:
+3
-1
@@ -6,6 +6,8 @@ It reads two SQLite databases:
|
||||
|
||||
- `TSS_BATTLES_DB` for `tss_battles.db`
|
||||
- `TSS_TEAMS_DB` for `tss_teams.db`
|
||||
- `BACKEND_HOST` bind host, default `127.0.0.1`
|
||||
- `BACKEND_ALLOWED_ORIGINS` comma-separated browser origins allowed by CORS
|
||||
|
||||
Both paths can be absolute or relative to the repo root when run through the root scripts/PM2.
|
||||
|
||||
@@ -25,7 +27,7 @@ It currently exposes:
|
||||
npm run dev:backend
|
||||
```
|
||||
|
||||
The backend listens on <http://localhost:6000> by default. Override with `BACKEND_PORT`.
|
||||
The backend listens on <http://127.0.0.1:6000> by default. Override with `BACKEND_PORT` and `BACKEND_HOST`.
|
||||
|
||||
## Production build
|
||||
|
||||
|
||||
+89
-57
@@ -1,6 +1,6 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::{header, Method, StatusCode},
|
||||
http::{header, HeaderValue, Method, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
routing::get,
|
||||
Json, Router,
|
||||
@@ -11,13 +11,13 @@ use serde_json::json;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env, fs,
|
||||
net::SocketAddr,
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
path::{Path as FsPath, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_http::{
|
||||
cors::{Any, CorsLayer},
|
||||
cors::{AllowOrigin, CorsLayer},
|
||||
trace::TraceLayer,
|
||||
};
|
||||
|
||||
@@ -273,6 +273,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
let host = env_ip("BACKEND_HOST").unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
let port = env_u16("BACKEND_PORT")
|
||||
.or_else(|| env_u16("PORT"))
|
||||
.unwrap_or(6000);
|
||||
@@ -292,13 +293,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.layer(
|
||||
CorsLayer::new()
|
||||
.allow_methods([Method::GET])
|
||||
.allow_origin(Any)
|
||||
.allow_origin(allowed_origins())
|
||||
.allow_headers([header::ACCEPT, header::CONTENT_TYPE]),
|
||||
)
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
||||
let addr = SocketAddr::from((host, port));
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
tracing::info!("tssbot backend listening on http://{}", addr);
|
||||
axum::serve(listener, app)
|
||||
@@ -727,22 +728,24 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result<Vec<PeriodHisto
|
||||
)
|
||||
.map_err(db_error)?;
|
||||
|
||||
stmt.query_map(params![team_id], |row| {
|
||||
let period: String = row.get(0)?;
|
||||
let battles: i64 = row.get(1)?;
|
||||
let wins: i64 = row.get(2)?;
|
||||
let losses: i64 = row.get(3)?;
|
||||
Ok(PeriodHistory {
|
||||
period,
|
||||
battles,
|
||||
wins,
|
||||
losses,
|
||||
win_rate: percent(wins, battles),
|
||||
let rows = stmt
|
||||
.query_map(params![team_id], |row| {
|
||||
let period: String = row.get(0)?;
|
||||
let battles: i64 = row.get(1)?;
|
||||
let wins: i64 = row.get(2)?;
|
||||
let losses: i64 = row.get(3)?;
|
||||
Ok(PeriodHistory {
|
||||
period,
|
||||
battles,
|
||||
wins,
|
||||
losses,
|
||||
win_rate: percent(wins, battles),
|
||||
})
|
||||
})
|
||||
})
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint>, ApiError> {
|
||||
@@ -755,15 +758,17 @@ fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint
|
||||
)
|
||||
.map_err(db_error)?;
|
||||
|
||||
stmt.query_map(params![team_id], |row| {
|
||||
Ok(RatingPoint {
|
||||
timestamp: row.get(0)?,
|
||||
rating: row.get(1)?,
|
||||
let rows = stmt
|
||||
.query_map(params![team_id], |row| {
|
||||
Ok(RatingPoint {
|
||||
timestamp: row.get(0)?,
|
||||
rating: row.get(1)?,
|
||||
})
|
||||
})
|
||||
})
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError> {
|
||||
@@ -798,36 +803,38 @@ fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError>
|
||||
)
|
||||
.map_err(db_error)?;
|
||||
|
||||
stmt.query_map(params![team_id], |row| {
|
||||
let session_id: String = row.get(0)?;
|
||||
let timestamp: i64 = row.get(1)?;
|
||||
let mission_mode: Option<String> = row.get(2)?;
|
||||
Ok(GameRow {
|
||||
session_id,
|
||||
timestamp,
|
||||
endtime_unix: timestamp,
|
||||
map_name: mission_mode.clone(),
|
||||
mission_mode,
|
||||
result: row.get(3)?,
|
||||
player_count: row.get(4)?,
|
||||
winning_team: row.get(14)?,
|
||||
losing_team: row.get(15)?,
|
||||
stats: GameStats {
|
||||
ground_kills: row.get(5)?,
|
||||
air_kills: row.get(6)?,
|
||||
assists: row.get(7)?,
|
||||
captures: row.get(8)?,
|
||||
deaths: row.get(9)?,
|
||||
score: row.get(10)?,
|
||||
missile_evades: row.get(11)?,
|
||||
shell_interceptions: row.get(12)?,
|
||||
team_kills_stat: row.get(13)?,
|
||||
},
|
||||
let rows = stmt
|
||||
.query_map(params![team_id], |row| {
|
||||
let session_id: String = row.get(0)?;
|
||||
let timestamp: i64 = row.get(1)?;
|
||||
let mission_mode: Option<String> = row.get(2)?;
|
||||
Ok(GameRow {
|
||||
session_id,
|
||||
timestamp,
|
||||
endtime_unix: timestamp,
|
||||
map_name: mission_mode.clone(),
|
||||
mission_mode,
|
||||
result: row.get(3)?,
|
||||
player_count: row.get(4)?,
|
||||
winning_team: row.get(14)?,
|
||||
losing_team: row.get(15)?,
|
||||
stats: GameStats {
|
||||
ground_kills: row.get(5)?,
|
||||
air_kills: row.get(6)?,
|
||||
assists: row.get(7)?,
|
||||
captures: row.get(8)?,
|
||||
deaths: row.get(9)?,
|
||||
score: row.get(10)?,
|
||||
missile_evades: row.get(11)?,
|
||||
shell_interceptions: row.get(12)?,
|
||||
team_kills_stat: row.get(13)?,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)
|
||||
.map_err(db_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(db_error)?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn validate_team_name(name: &str) -> Result<&str, ApiError> {
|
||||
@@ -875,6 +882,31 @@ fn env_u16(key: &str) -> Option<u16> {
|
||||
env::var(key).ok()?.parse().ok()
|
||||
}
|
||||
|
||||
fn env_ip(key: &str) -> Option<IpAddr> {
|
||||
env::var(key).ok()?.parse().ok()
|
||||
}
|
||||
|
||||
fn allowed_origins() -> AllowOrigin {
|
||||
let origins = env::var("BACKEND_ALLOWED_ORIGINS")
|
||||
.or_else(|_| env::var("PUBLIC_ORIGIN"))
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.filter_map(|origin| {
|
||||
let origin = origin.trim();
|
||||
if origin.is_empty() {
|
||||
return None;
|
||||
}
|
||||
HeaderValue::from_str(origin).ok()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if origins.is_empty() {
|
||||
AllowOrigin::list(Vec::<HeaderValue>::new())
|
||||
} else {
|
||||
AllowOrigin::list(origins)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_db_path(env_key: &str, default_file: &str) -> PathBuf {
|
||||
let raw = env::var(env_key).unwrap_or_else(|_| default_file.to_string());
|
||||
let expanded = expand_home(&raw);
|
||||
|
||||
Reference in New Issue
Block a user