Harden Rust backend deployment
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
node_modules
|
node_modules
|
||||||
|
backend/target
|
||||||
dist
|
dist
|
||||||
dist-next
|
dist-next
|
||||||
dist-previous
|
dist-previous
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
.env.backup-*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
vite-dev*.log
|
vite-dev*.log
|
||||||
|
|||||||
@@ -41,8 +41,9 @@ Run the Rust backend separately:
|
|||||||
npm run dev:backend
|
npm run dev:backend
|
||||||
```
|
```
|
||||||
|
|
||||||
The backend listens on <http://localhost:6000> by default and reads the SQLite
|
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`.
|
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
|
## Production with PM2
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -6,6 +6,8 @@ It reads two SQLite databases:
|
|||||||
|
|
||||||
- `TSS_BATTLES_DB` for `tss_battles.db`
|
- `TSS_BATTLES_DB` for `tss_battles.db`
|
||||||
- `TSS_TEAMS_DB` for `tss_teams.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.
|
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
|
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
|
## Production build
|
||||||
|
|
||||||
|
|||||||
+43
-11
@@ -1,6 +1,6 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
http::{header, Method, StatusCode},
|
http::{header, HeaderValue, Method, StatusCode},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
Json, Router,
|
Json, Router,
|
||||||
@@ -11,13 +11,13 @@ use serde_json::json;
|
|||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
env, fs,
|
env, fs,
|
||||||
net::SocketAddr,
|
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||||
path::{Path as FsPath, PathBuf},
|
path::{Path as FsPath, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tower_http::{
|
use tower_http::{
|
||||||
cors::{Any, CorsLayer},
|
cors::{AllowOrigin, CorsLayer},
|
||||||
trace::TraceLayer,
|
trace::TraceLayer,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -273,6 +273,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
let host = env_ip("BACKEND_HOST").unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||||
let port = env_u16("BACKEND_PORT")
|
let port = env_u16("BACKEND_PORT")
|
||||||
.or_else(|| env_u16("PORT"))
|
.or_else(|| env_u16("PORT"))
|
||||||
.unwrap_or(6000);
|
.unwrap_or(6000);
|
||||||
@@ -292,13 +293,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.layer(
|
.layer(
|
||||||
CorsLayer::new()
|
CorsLayer::new()
|
||||||
.allow_methods([Method::GET])
|
.allow_methods([Method::GET])
|
||||||
.allow_origin(Any)
|
.allow_origin(allowed_origins())
|
||||||
.allow_headers([header::ACCEPT, header::CONTENT_TYPE]),
|
.allow_headers([header::ACCEPT, header::CONTENT_TYPE]),
|
||||||
)
|
)
|
||||||
.layer(TraceLayer::new_for_http())
|
.layer(TraceLayer::new_for_http())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let addr = SocketAddr::from(([0, 0, 0, 0], port));
|
let addr = SocketAddr::from((host, port));
|
||||||
let listener = TcpListener::bind(addr).await?;
|
let listener = TcpListener::bind(addr).await?;
|
||||||
tracing::info!("tssbot backend listening on http://{}", addr);
|
tracing::info!("tssbot backend listening on http://{}", addr);
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
@@ -727,7 +728,8 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result<Vec<PeriodHisto
|
|||||||
)
|
)
|
||||||
.map_err(db_error)?;
|
.map_err(db_error)?;
|
||||||
|
|
||||||
stmt.query_map(params![team_id], |row| {
|
let rows = stmt
|
||||||
|
.query_map(params![team_id], |row| {
|
||||||
let period: String = row.get(0)?;
|
let period: String = row.get(0)?;
|
||||||
let battles: i64 = row.get(1)?;
|
let battles: i64 = row.get(1)?;
|
||||||
let wins: i64 = row.get(2)?;
|
let wins: i64 = row.get(2)?;
|
||||||
@@ -742,7 +744,8 @@ fn period_history_for(conn: &Connection, team_id: i64) -> Result<Vec<PeriodHisto
|
|||||||
})
|
})
|
||||||
.map_err(db_error)?
|
.map_err(db_error)?
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(db_error)
|
.map_err(db_error)?;
|
||||||
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint>, ApiError> {
|
fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint>, ApiError> {
|
||||||
@@ -755,7 +758,8 @@ fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint
|
|||||||
)
|
)
|
||||||
.map_err(db_error)?;
|
.map_err(db_error)?;
|
||||||
|
|
||||||
stmt.query_map(params![team_id], |row| {
|
let rows = stmt
|
||||||
|
.query_map(params![team_id], |row| {
|
||||||
Ok(RatingPoint {
|
Ok(RatingPoint {
|
||||||
timestamp: row.get(0)?,
|
timestamp: row.get(0)?,
|
||||||
rating: row.get(1)?,
|
rating: row.get(1)?,
|
||||||
@@ -763,7 +767,8 @@ fn rating_history_for(conn: &Connection, team_id: i64) -> Result<Vec<RatingPoint
|
|||||||
})
|
})
|
||||||
.map_err(db_error)?
|
.map_err(db_error)?
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(db_error)
|
.map_err(db_error)?;
|
||||||
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError> {
|
fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError> {
|
||||||
@@ -798,7 +803,8 @@ fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError>
|
|||||||
)
|
)
|
||||||
.map_err(db_error)?;
|
.map_err(db_error)?;
|
||||||
|
|
||||||
stmt.query_map(params![team_id], |row| {
|
let rows = stmt
|
||||||
|
.query_map(params![team_id], |row| {
|
||||||
let session_id: String = row.get(0)?;
|
let session_id: String = row.get(0)?;
|
||||||
let timestamp: i64 = row.get(1)?;
|
let timestamp: i64 = row.get(1)?;
|
||||||
let mission_mode: Option<String> = row.get(2)?;
|
let mission_mode: Option<String> = row.get(2)?;
|
||||||
@@ -827,7 +833,8 @@ fn games_for(conn: &Connection, team_id: i64) -> Result<Vec<GameRow>, ApiError>
|
|||||||
})
|
})
|
||||||
.map_err(db_error)?
|
.map_err(db_error)?
|
||||||
.collect::<Result<Vec<_>, _>>()
|
.collect::<Result<Vec<_>, _>>()
|
||||||
.map_err(db_error)
|
.map_err(db_error)?;
|
||||||
|
Ok(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_team_name(name: &str) -> Result<&str, ApiError> {
|
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()
|
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 {
|
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 raw = env::var(env_key).unwrap_or_else(|_| default_file.to_string());
|
||||||
let expanded = expand_home(&raw);
|
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 = {
|
module.exports = {
|
||||||
apps: [
|
apps: [
|
||||||
{
|
{
|
||||||
@@ -52,6 +84,8 @@ module.exports = {
|
|||||||
env: {
|
env: {
|
||||||
NODE_ENV: 'production',
|
NODE_ENV: 'production',
|
||||||
BACKEND_PORT: process.env.BACKEND_PORT || 6000,
|
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_BATTLES_DB: process.env.TSS_BATTLES_DB || 'tss_battles.db',
|
||||||
TSS_TEAMS_DB: process.env.TSS_TEAMS_DB || 'tss_teams.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
|
PUBLIC_ORIGIN=https://example.com
|
||||||
|
|
||||||
BACKEND_PORT=6000
|
BACKEND_PORT=6000
|
||||||
|
BACKEND_HOST=127.0.0.1
|
||||||
|
BACKEND_ALLOWED_ORIGINS=https://example.com
|
||||||
TSS_BATTLES_DB=./tss_battles.db
|
TSS_BATTLES_DB=./tss_battles.db
|
||||||
TSS_TEAMS_DB=./tss_teams.db
|
TSS_TEAMS_DB=./tss_teams.db
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user