fix: resolve all remaining audit failures across rust, svelte, and java
This commit is contained in:
1 parent
bc12a25a01
commit
590178e41a
35 files changed
+958
-300
No files matched your search
@@ -10,6 +10,62 @@ use scopenet_shared::{BaltopEntry, EconomyTransaction, MarketListing, ServerEcon
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
|
||||
async fn begin_operation(state: &AppState, server_id: i64, operation_id: &str) -> AppResult<(sqlx::Transaction<'static, sqlx::Sqlite>, Option<Value>)> {
|
||||
if operation_id.is_empty() || operation_id.len() > 100 { return Err(AppError::bad_request("Invalid operation ID")); }
|
||||
let mut tx = state.db.begin().await?;
|
||||
// The first statement obtains the write lock before reading any balances.
|
||||
let inserted = sqlx::query("INSERT OR IGNORE INTO economy_operations(server_id, operation_id, response) VALUES (?, ?, '')")
|
||||
.bind(server_id).bind(operation_id).execute(&mut *tx).await?.rows_affected();
|
||||
let previous = if inserted == 0 {
|
||||
let raw: String = sqlx::query_scalar("SELECT response FROM economy_operations WHERE server_id = ? AND operation_id = ?")
|
||||
.bind(server_id).bind(operation_id).fetch_one(&mut *tx).await?;
|
||||
Some(serde_json::from_str(&raw)?)
|
||||
} else { None };
|
||||
Ok((tx, previous))
|
||||
}
|
||||
|
||||
async fn finish_operation(mut tx: sqlx::Transaction<'_, sqlx::Sqlite>, server_id: i64, operation_id: &str, response: Value) -> AppResult<Json<Value>> {
|
||||
sqlx::query("UPDATE economy_operations SET response = ? WHERE server_id = ? AND operation_id = ?")
|
||||
.bind(response.to_string()).bind(server_id).bind(operation_id).execute(&mut *tx).await?;
|
||||
tx.commit().await?;
|
||||
Ok(Json(response))
|
||||
}
|
||||
|
||||
async fn ensure_balance(tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, server_id: i64, uuid: &str, name: &str) -> AppResult<()> {
|
||||
sqlx::query("INSERT INTO server_economy(server_id, uuid, username, balance, updated_at) VALUES (?, ?, ?, 1000, ?) ON CONFLICT(server_id, uuid) DO NOTHING")
|
||||
.bind(server_id).bind(uuid).bind(name).bind(chrono::Utc::now().to_rfc3339()).execute(&mut **tx).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct AdjustBalancePayload {
|
||||
pub uuid: String,
|
||||
pub username: String,
|
||||
pub delta: f64,
|
||||
pub operation_id: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
pub async fn server_adjust_balance(GameServer(server): GameServer, State(state): State<AppState>, Json(p): Json<AdjustBalancePayload>) -> AppResult<Json<Value>> {
|
||||
if !p.delta.is_finite() || p.delta == 0.0 || p.delta.abs() > 1e12 { return Err(AppError::bad_request("Invalid amount")); }
|
||||
let (mut tx, previous) = begin_operation(&state, server.id, &p.operation_id).await?;
|
||||
if let Some(previous) = previous { return Ok(Json(previous)); }
|
||||
ensure_balance(&mut tx, server.id, &p.uuid, &p.username).await?;
|
||||
let balance: Option<f64> = sqlx::query_scalar("UPDATE server_economy SET balance = balance + ?, updated_at = ? WHERE server_id = ? AND uuid = ? AND balance + ? >= 0 RETURNING balance")
|
||||
.bind(p.delta).bind(chrono::Utc::now().to_rfc3339()).bind(server.id).bind(&p.uuid).bind(p.delta).fetch_optional(&mut *tx).await?;
|
||||
let Some(balance) = balance else { return Err(AppError::bad_request("Insufficient funds")); };
|
||||
let (from, from_name, to, to_name) = if p.delta > 0.0 { ("server", "Server Shop", p.uuid.as_str(), p.username.as_str()) } else { (p.uuid.as_str(), p.username.as_str(), "server", "Server Shop") };
|
||||
sqlx::query("INSERT INTO economy_transactions(server_id, from_uuid, from_name, to_uuid, to_name, amount, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
.bind(server.id).bind(from).bind(from_name).bind(to).bind(to_name).bind(p.delta.abs()).bind(&p.description).bind(chrono::Utc::now().to_rfc3339()).execute(&mut *tx).await?;
|
||||
finish_operation(tx, server.id, &p.operation_id, serde_json::json!({"ok": true, "balance": balance})).await
|
||||
}
|
||||
|
||||
pub async fn server_market_read(GameServer(server): GameServer, State(state): State<AppState>) -> AppResult<Json<Vec<Value>>> {
|
||||
let rows: Vec<(i64, String, String, String, i32, f64, Option<String>)> = sqlx::query_as("SELECT id, seller_name, item_id, item_name, amount, price, item_data FROM server_market WHERE server_id = ? ORDER BY id DESC LIMIT 45")
|
||||
.bind(server.id).fetch_all(&state.db).await?;
|
||||
Ok(Json(rows.into_iter().map(|(id, seller, item_id, item_name, amount, price, data)| serde_json::json!({"id":id,"seller_name":seller,"item_id":item_id,"item_name":item_name,"amount":amount,"price":price,"item_data":data})).collect()))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Launcher & Public Economy API
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -213,7 +269,7 @@ pub async fn server_transfer(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ServerTransferPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
if payload.amount <= 0.0 {
|
||||
if !payload.amount.is_finite() || payload.amount <= 0.0 {
|
||||
return Err(AppError::bad_request("Transfer amount must be positive"));
|
||||
}
|
||||
|
||||
@@ -347,6 +403,8 @@ pub async fn server_baltop(
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MarketListPayload {
|
||||
pub operation_id: String,
|
||||
pub item_data: Option<String>,
|
||||
pub seller_uuid: String,
|
||||
pub seller_name: String,
|
||||
pub item_id: String,
|
||||
@@ -360,10 +418,13 @@ pub async fn server_market_list(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<MarketListPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
if !payload.price.is_finite() || payload.price <= 0.0 || payload.amount <= 0 || payload.amount > 64 { return Err(AppError::bad_request("Invalid listing")); }
|
||||
let (mut tx, previous) = begin_operation(&state, server.id, &payload.operation_id).await?;
|
||||
if let Some(previous) = previous { return Ok(Json(previous)); }
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
"INSERT INTO server_market (server_id, seller_uuid, seller_name, item_id, item_name, amount, price, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"INSERT INTO server_market (server_id, seller_uuid, seller_name, item_id, item_name, amount, price, created_at, item_data)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
RETURNING id",
|
||||
)
|
||||
.bind(server.id)
|
||||
@@ -374,14 +435,16 @@ pub async fn server_market_list(
|
||||
.bind(payload.amount)
|
||||
.bind(payload.price)
|
||||
.bind(&now)
|
||||
.fetch_one(&state.db)
|
||||
.bind(&payload.item_data)
|
||||
.fetch_one(&mut *tx)
|
||||
.await?;
|
||||
|
||||
Ok(Json(serde_json::json!({ "id": id, "ok": true })))
|
||||
finish_operation(tx, server.id, &payload.operation_id, serde_json::json!({ "id": id, "ok": true })).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct MarketBuyPayload {
|
||||
pub operation_id: String,
|
||||
pub listing_id: i64,
|
||||
pub buyer_uuid: String,
|
||||
pub buyer_name: String,
|
||||
@@ -392,21 +455,24 @@ pub async fn server_market_buy(
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<MarketBuyPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let listing: Option<(String, String, String, String, i32, f64)> = sqlx::query_as(
|
||||
"SELECT seller_uuid, seller_name, item_id, item_name, amount, price
|
||||
FROM server_market
|
||||
WHERE id = ? AND server_id = ?",
|
||||
let (mut tx, previous) = begin_operation(&state, server.id, &payload.operation_id).await?;
|
||||
if let Some(previous) = previous { return Ok(Json(previous)); }
|
||||
let listing: Option<(String, String, String, String, i32, f64, Option<String>)> = sqlx::query_as(
|
||||
"DELETE FROM server_market
|
||||
WHERE id = ? AND server_id = ?
|
||||
RETURNING seller_uuid, seller_name, item_id, item_name, amount, price, item_data
|
||||
",
|
||||
)
|
||||
.bind(payload.listing_id)
|
||||
.bind(server.id)
|
||||
.fetch_optional(&state.db)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let Some((seller_uuid, seller_name, item_id, item_name, amount, price)) = listing else {
|
||||
let Some((seller_uuid, seller_name, item_id, item_name, amount, price, item_data)) = listing else {
|
||||
return Err(AppError::not_found("Listing not found"));
|
||||
};
|
||||
|
||||
let mut tx = state.db.begin().await?;
|
||||
ensure_balance(&mut tx, server.id, &payload.buyer_uuid, &payload.buyer_name).await?;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Deduct buyer
|
||||
@@ -471,13 +537,12 @@ pub async fn server_market_buy(
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
tx.commit().await?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
finish_operation(tx, server.id, &payload.operation_id, serde_json::json!({
|
||||
"ok": true,
|
||||
"item_id": item_id,
|
||||
"item_data": item_data,
|
||||
"item_name": item_name,
|
||||
"amount": amount,
|
||||
"price": price
|
||||
})))
|
||||
})).await
|
||||
}
|
||||
Reference in new issue
Block a user