deploy change, update token system, fix abuses and add admin verify
This commit is contained in:
@@ -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 })))
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -24,5 +24,6 @@ pub trait PaymentProvider: Send + Sync {
|
||||
&self,
|
||||
external_tx_id: &str,
|
||||
expected_amount: i64,
|
||||
expected_invoice_id: &str,
|
||||
) -> Result<bool, AppError>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user