deploy change, update token system, fix abuses and add admin verify

This commit is contained in:
2026-05-26 12:50:25 +07:00
parent 1ef93c2025
commit 26d6f2db8a
25 changed files with 416 additions and 557 deletions
+9
View File
@@ -1,4 +1,6 @@
use crate::{api::ApiState, error::AppError};
use axum::http::header::SET_COOKIE;
use axum::http::HeaderMap;
use axum::response::IntoResponse;
use axum::{extract::State, routing::post, Json, Router};
use serde::Deserialize;
@@ -56,6 +58,13 @@ async fn auth_telegram_api(
.auth_service
.generate_auth_token(user.id, user.tg_id, &state.jwt_secret)?;
let mut headers = HeaderMap::new();
let cookie = format!(
"nrxp_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=600",
token
);
headers.insert(SET_COOKIE, cookie.parse().unwrap());
Ok(Json(json!({ "status": "success", "token": token })))
}
+18 -8
View File
@@ -16,23 +16,32 @@ pub async fn auth_guard(
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.filter(|h| h.starts_with("Bearer "))
.map(|h| &h[7..])
.ok_or_else(|| AppError::Unauthorized("Access token required".into()))?;
.and_then(|h| h.to_str().ok());
// 1. Получаем чистый токен (String)
let token = if let Some(h) = auth_header {
h.replace("Bearer ", "")
} else {
req.headers()
.get("Cookie")
.and_then(|c| c.to_str().ok())
.and_then(|c| c.split("; ").find(|part| part.starts_with("nrxp_token=")))
.map(|part| part.replace("nrxp_token=", ""))
.ok_or_else(|| AppError::Unauthorized("Access token required".into()))?
};
// 2. Декодируем токен, используя полученную выше переменную `token`
// Передаем ссылку &token, так как jsonwebtoken ожидает &str
let token_data = jsonwebtoken::decode::<super::service::Claims>(
auth_header,
&token,
&jsonwebtoken::DecodingKey::from_secret(state.jwt_secret.as_bytes()),
&jsonwebtoken::Validation::default(),
)
.map_err(|_| AppError::Unauthorized("Invalid session signature".into()))?;
// Парсим внутренний UUID из поля sub токена
let user_id = uuid::Uuid::parse_str(&token_data.claims.sub)
.map_err(|_| AppError::Unauthorized("Invalid token payload structure".into()))?;
// Ищем оперативника в сети по его системному UUID
let user = state
.repo
.get_user_by_id(user_id)
@@ -51,7 +60,8 @@ pub async fn admin_guard(req: Request, next: Next) -> Result<Response, AppError>
.get::<User>()
.ok_or_else(|| AppError::Unauthorized("Context lost: Auth required".into()))?;
if user.role != "admin" {
let admin_id: i64 = std::env::var("ADMIN_TG_ID").unwrap().parse().unwrap();
if user.tg_id == Some(admin_id) || user.role == "admin" {
warn!(
"SECURITY_ALERT: Unauthorized admin access attempt by @{:?}",
user.tg_username
+2 -1
View File
@@ -41,7 +41,8 @@ impl AuthService {
let claims = Claims {
sub: user_id.to_string(),
tg_id,
exp: (Utc::now() + chrono::Duration::hours(24)).timestamp() as usize,
// Время жизни токена сокращено до 10 минут
exp: (Utc::now() + chrono::Duration::minutes(10)).timestamp() as usize,
};
encode(
+1
View File
@@ -24,5 +24,6 @@ pub trait PaymentProvider: Send + Sync {
&self,
external_tx_id: &str,
expected_amount: i64,
expected_invoice_id: &str,
) -> Result<bool, AppError>;
}
+13 -14
View File
@@ -48,7 +48,12 @@ impl PaymentProvider for TonProvider {
}))
}
async fn verify_payment(&self, tx_hash: &str, expected_amount: i64) -> Result<bool, AppError> {
async fn verify_payment(
&self,
tx_hash: &str,
expected_amount: i64,
expected_invoice_id: &str,
) -> Result<bool, AppError> {
let client = reqwest::Client::new();
let url = format!("https://tonapi.io/v2/blockchain/transactions/{}", tx_hash);
@@ -67,27 +72,21 @@ impl PaymentProvider for TonProvider {
.await
.map_err(|e| AppError::Internal(format!("Ошибка парсинга JSON от TON API: {}", e)))?;
// Проверяем входящее сообщение транзакции
let in_msg = &data["in_msg"];
if in_msg.is_null() {
return Ok(false);
}
let value = in_msg["value"].as_i64().unwrap_or(0);
let dest = in_msg["destination"]["address"].as_str().unwrap_or("");
// В боевой версии также стоит проверять:
// in_msg["decoded_body"]["text"] == ID инвойса, чтобы избежать оплаты чужих инвойсов
let comment = in_msg["decoded_body"]["text"].as_str().unwrap_or("");
// Сверяем кошелек назначения и сумму (с допуском на мелкие расхождения/комиссии, если нужно)
if value >= expected_amount && dest == self.master_wallet {
tracing::info!("TON Transaction {} verified successfully!", tx_hash);
if value >= expected_amount && dest == self.master_wallet && comment == expected_invoice_id
{
tracing::info!("TON Transaction {} verified!", tx_hash);
Ok(true)
} else {
tracing::warn!(
"TON Transaction mismatch. Expected: {}, Got: {}",
expected_amount,
value
"TON Mismatch. Expected ID: {}, Got Comment: {}",
expected_invoice_id,
comment
);
Ok(false)
}
+11 -9
View File
@@ -18,14 +18,14 @@ pub struct BillingService {
// Добавляем Clone вручную, так как dyn Traits не поддерживают derive(Clone) легко
impl Clone for BillingService {
fn clone(&self) -> Self {
// ВАЖНО: В реальном проекте провайдеры стоит обернуть в Arc,
// но для простоты структуры мы пересоздаем их при клонировании сервиса
// Снова читаем из env при клонировании
let master_wallet = std::env::var("TON_MASTER_WALLET")
.expect("КРИТИЧЕСКАЯ ОШИБКА: TON_MASTER_WALLET не задан в .env!");
let mut providers: HashMap<&'static str, Box<dyn PaymentProvider>> = HashMap::new();
providers.insert(
"TON",
Box::new(super::providers::ton::TonProvider {
master_wallet: "UQ_MASTER_WALLET_ADDRESS".into(),
}),
Box::new(super::providers::ton::TonProvider { master_wallet }),
);
Self {
@@ -37,12 +37,14 @@ impl Clone for BillingService {
impl BillingService {
pub fn new(repo: Arc<dyn AppRepository>) -> Self {
// Читаем адрес из переменной окружения
let master_wallet = std::env::var("TON_MASTER_WALLET")
.expect("КРИТИЧЕСКАЯ ОШИБКА: TON_MASTER_WALLET не задан в .env!");
let mut providers: HashMap<&'static str, Box<dyn PaymentProvider>> = HashMap::new();
providers.insert(
"TON",
Box::new(super::providers::ton::TonProvider {
master_wallet: "UQ_MASTER_WALLET_ADDRESS".into(),
}),
Box::new(super::providers::ton::TonProvider { master_wallet }),
);
Self { repo, providers }
@@ -135,7 +137,7 @@ impl BillingService {
// 3. Провайдер лезет в блокчейн и проверяет транзакцию
let is_valid = provider
.verify_payment(external_tx_id, invoice.amount)
.verify_payment(external_tx_id, invoice.amount, &invoice.id.to_string()) // <--- ПЕРЕДАЕМ ID
.await?;
if !is_valid {
+14 -2
View File
@@ -11,15 +11,20 @@ use uuid::Uuid;
use ssh2::Session;
use std::io::Read;
use std::net::TcpStream;
use tokio::sync::Semaphore;
#[derive(Clone)]
pub struct NodeService {
repo: Arc<dyn AppRepository>,
deploy_semaphore: Arc<Semaphore>,
}
impl NodeService {
pub fn new(repo: Arc<dyn AppRepository>) -> Self {
Self { repo }
Self {
repo,
deploy_semaphore: Arc::new(Semaphore::new(3)),
}
}
pub async fn get_active_nodes(&self) -> Result<Vec<VpnNode>, AppError> {
@@ -37,7 +42,13 @@ impl NodeService {
let ip_address = payload.ip_address.clone();
let password = ssh_password.to_string();
// Orchestrate SSH asynchronously so it does not block the Axum runtime
// Ждем разрешения от семафора
let _permit = self
.deploy_semaphore
.acquire()
.await
.map_err(|_| AppError::Internal("Semaphore closed".into()))?;
tokio::task::spawn_blocking(move || Self::provision_node_via_ssh(&ip_address, &password))
.await
.map_err(|_| AppError::Internal("SSH Task panicked".into()))??;
@@ -45,6 +56,7 @@ impl NodeService {
let node = self.repo.add_vpn_node(payload).await?;
Ok(node)
}
fn provision_node_via_ssh(ip: &str, password: &str) -> Result<(), AppError> {
use std::fs;
use std::net::{SocketAddr, TcpStream};