Harden Rust backend deployment
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
node_modules
|
||||
backend/target
|
||||
dist
|
||||
dist-next
|
||||
dist-previous
|
||||
.env
|
||||
.env.local
|
||||
.env.backup-*
|
||||
.DS_Store
|
||||
npm-debug.log*
|
||||
vite-dev*.log
|
||||
|
||||
@@ -41,8 +41,9 @@ Run the Rust backend separately:
|
||||
npm run dev:backend
|
||||
```
|
||||
|
||||
The backend listens on <http://localhost:6000> by default and reads the SQLite
|
||||
databases configured by `TSS_BATTLES_DB` and `TSS_TEAMS_DB`.
|
||||
The backend listens on <http://127.0.0.1:6000> by default and reads the SQLite
|
||||
databases configured by `TSS_BATTLES_DB` and `TSS_TEAMS_DB`. Keep it bound to
|
||||
`127.0.0.1` in production and let `tssbot-web` proxy public API requests.
|
||||
|
||||
## Production with PM2
|
||||
|
||||
|
||||
+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);
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
|
||||
function loadEnvFile() {
|
||||
const envPath = path.join(__dirname, '.env')
|
||||
if (!fs.existsSync(envPath)) return
|
||||
|
||||
const lines = fs.readFileSync(envPath, 'utf8').split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed || trimmed.startsWith('#')) continue
|
||||
|
||||
const separatorIndex = trimmed.indexOf('=')
|
||||
if (separatorIndex === -1) continue
|
||||
|
||||
const key = trimmed.slice(0, separatorIndex).trim()
|
||||
let value = trimmed.slice(separatorIndex + 1).trim()
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1)
|
||||
}
|
||||
|
||||
if (key && (!process.env[key] || process.env[key] === '')) {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile()
|
||||
|
||||
module.exports = {
|
||||
apps: [
|
||||
{
|
||||
@@ -52,6 +84,8 @@ module.exports = {
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
BACKEND_PORT: process.env.BACKEND_PORT || 6000,
|
||||
BACKEND_HOST: process.env.BACKEND_HOST || '127.0.0.1',
|
||||
BACKEND_ALLOWED_ORIGINS: process.env.BACKEND_ALLOWED_ORIGINS || process.env.PUBLIC_ORIGIN || '',
|
||||
TSS_BATTLES_DB: process.env.TSS_BATTLES_DB || 'tss_battles.db',
|
||||
TSS_TEAMS_DB: process.env.TSS_TEAMS_DB || 'tss_teams.db',
|
||||
},
|
||||
|
||||
@@ -6,6 +6,8 @@ API_UPSTREAM=http://127.0.0.1:6000
|
||||
PUBLIC_ORIGIN=https://example.com
|
||||
|
||||
BACKEND_PORT=6000
|
||||
BACKEND_HOST=127.0.0.1
|
||||
BACKEND_ALLOWED_ORIGINS=https://example.com
|
||||
TSS_BATTLES_DB=./tss_battles.db
|
||||
TSS_TEAMS_DB=./tss_teams.db
|
||||
|
||||
|
||||
Reference in New Issue
Block a user