fix(billing): resolve all audit findings — CSRF, float precision, TOCTOU, error sanitization
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
- Add CSRF token protection for mock payment (SHA256 + constant-time verify) - Replace f64 currency conversion with pure integer string parsing (parse_yuan_to_cents) - Move subscription check inside transaction to prevent TOCTOU race - Rewrite increment_usage to use atomic SQL (account_id+period_start WHERE) - Add trade_no format validation in payment callback - Sanitize error messages to prevent sensitive data leakage - Use i32::try_from for WeChat amount conversion (prevent truncation) - Replace window.__ZCLAW_STATS_SYNC_INTERVAL__ with useRef pattern - Replace eprintln/println with tracing macros in lifecycle - Remove unused variable in scheduler - Remove duplicate sha2 and unused hmac from Cargo.toml
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! 计费服务层 — 计划查询、订阅管理、用量检查
|
||||
|
||||
use chrono::{Datelike, Timelike};
|
||||
use chrono::{Datelike, Timelike, Utc};
|
||||
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::error::SaasResult;
|
||||
@@ -176,6 +177,7 @@ pub async fn get_or_create_usage(pool: &PgPool, account_id: &str) -> SaasResult<
|
||||
/// 增加用量计数(Relay 请求:tokens + relay_requests +1)
|
||||
///
|
||||
/// 在 relay handler 响应成功后直接调用,实现实时配额更新。
|
||||
/// 使用 INSERT ON CONFLICT 确保配额行存在,单条原子 UPDATE 避免竞态。
|
||||
/// 聚合器 `AggregateUsageWorker` 每小时做一次对账修正。
|
||||
pub async fn increment_usage(
|
||||
pool: &PgPool,
|
||||
@@ -183,18 +185,30 @@ pub async fn increment_usage(
|
||||
input_tokens: i64,
|
||||
output_tokens: i64,
|
||||
) -> SaasResult<()> {
|
||||
let usage = get_or_create_usage(pool, account_id).await?;
|
||||
// 确保 quota 行存在(幂等)
|
||||
let _ = get_or_create_usage(pool, account_id).await?;
|
||||
|
||||
// 直接用 account_id + period 原子更新,无需 SELECT 获取 ID
|
||||
let now = chrono::Utc::now();
|
||||
let period_start = now
|
||||
.with_day(1).unwrap_or(now)
|
||||
.with_hour(0).unwrap_or(now)
|
||||
.with_minute(0).unwrap_or(now)
|
||||
.with_second(0).unwrap_or(now)
|
||||
.with_nanosecond(0).unwrap_or(now);
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE billing_usage_quotas \
|
||||
SET input_tokens = input_tokens + $1, \
|
||||
output_tokens = output_tokens + $2, \
|
||||
relay_requests = relay_requests + 1, \
|
||||
updated_at = NOW() \
|
||||
WHERE id = $3"
|
||||
WHERE account_id = $3 AND period_start = $4"
|
||||
)
|
||||
.bind(input_tokens)
|
||||
.bind(output_tokens)
|
||||
.bind(&usage.id)
|
||||
.bind(account_id)
|
||||
.bind(period_start)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
@@ -227,7 +241,7 @@ pub async fn increment_dimension(
|
||||
).bind(&usage.id).execute(pool).await?;
|
||||
}
|
||||
_ => return Err(crate::error::SaasError::InvalidInput(
|
||||
format!("Unknown usage dimension: {}", dimension)
|
||||
"Unknown usage dimension".into()
|
||||
)),
|
||||
}
|
||||
Ok(())
|
||||
@@ -261,7 +275,7 @@ pub async fn increment_dimension_by(
|
||||
).bind(count).bind(&usage.id).execute(pool).await?;
|
||||
}
|
||||
_ => return Err(crate::error::SaasError::InvalidInput(
|
||||
format!("Unknown usage dimension: {}", dimension)
|
||||
"Unknown usage dimension".into()
|
||||
)),
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user