(feat) added handler

This commit is contained in:
Rorik Star Platinum 2025-11-05 17:30:50 +03:00
parent 5b3c2d4ec7
commit de7c0f7110
8 changed files with 203 additions and 32 deletions

1
Cargo.lock generated
View file

@ -1227,6 +1227,7 @@ dependencies = [
"tokio",
"tracing",
"tracing-subscriber",
"url",
"uuid",
]

View file

@ -7,6 +7,7 @@ edition = "2024"
axum = "0.8"
tokio = { version = "1.48", features = ["full"] }
reqwest = { version = "0.12", features = ["json"] }
url = "2.5"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }
dotenvy = "0.15"

View file

@ -1,4 +1,8 @@
// src/api.rs
// Объявляет клиенты для работы с внешними API
// This file is the public-facing module for all external API interactions.
pub mod banking; // Наш HTTP клиент к банковским API
// Make the banking submodule public.
pub mod banking;
// Re-export the primary client struct so other modules can use it via `crate::api::*`.
pub use banking::BankingClients;

View file

@ -1,46 +1,204 @@
// src/api/banking.rs
use reqwest::Client as HttpClient;
use std::sync::Arc;
use serde::Deserialize;
use std::env;
use std::str::FromStr;
use std::sync::Arc; // Import Arc
use tokio::sync::RwLock;
use url::Url;
use chrono::{DateTime, Duration, Utc};
// Модели и ошибки можно вынести в отдельные файлы в api/
// Например, api/banking_models.rs
// --- Public Enums and Errors ---
#[derive(Debug, serde::Deserialize)]
pub struct BankToken {
pub access_token: String,
pub expires_in: i64,
#[derive(Debug, thiserror::Error)]
pub enum BankingError {
#[error("Environment variable not set: {0}")]
MissingEnvVar(String),
#[error("Invalid URL: {0}")]
UrlParseError(#[from] url::ParseError),
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("API returned an error: {status} {body}")]
ApiError { status: u16, body: String },
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
pub enum Bank {
VBank,
ABank,
SBank,
}
impl Bank {
// ...
// --- Data Models ---
#[derive(Debug, Deserialize)]
pub struct BankTokenResponse {
pub access_token: String,
pub expires_in: i64,
}
#[derive(Debug, Clone)]
struct StoredToken {
access_token: String,
expires_at: DateTime<Utc>,
}
impl StoredToken {
fn is_valid(&self) -> bool {
// Check if the token is valid for at least the next 60 seconds.
self.expires_at > Utc::now() + Duration::seconds(60)
}
}
// --- Client Implementation ---
#[derive(Clone)]
pub struct BankClient {
// ...
}
impl BankClient {
// ...
http_client: HttpClient,
base_url: Url,
client_id: String,
client_secret: String,
// FIX: Wrap the RwLock in an Arc to make it shareable and clonable.
token: Arc<RwLock<Option<StoredToken>>>,
}
#[derive(Clone)]
pub struct BankingClients {
pub vbank: BankClient,
pub abank: BankClient,
pub sbank: BankClient,
pub vbank: Arc<BankClient>,
pub abank: Arc<BankClient>,
pub sbank: Arc<BankClient>,
}
// --- impl Blocks ---
impl FromStr for Bank {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"vbank" => Ok(Bank::VBank),
"abank" => Ok(Bank::ABank),
"sbank" => Ok(Bank::SBank),
_ => Err(anyhow::anyhow!("Unknown bank: {}", s)),
}
}
}
impl BankClient {
pub fn new(
http_client: HttpClient,
base_url: Url,
client_id: String,
client_secret: String,
) -> Self {
Self {
http_client,
base_url,
client_id,
client_secret,
// FIX: Initialize the Arc<RwLock<...>>
token: Arc::new(RwLock::new(None)),
}
}
/// Fetches a new token from the bank's API.
async fn fetch_new_token(&self) -> Result<StoredToken, BankingError> {
println!("🔄 Requesting new token for bank: {}", self.base_url.host_str().unwrap_or(""));
let token_url = self.base_url.join("/auth/bank-token")?;
let response = self
.http_client
.post(token_url)
.query(&[
("client_id", self.client_id.as_str()),
("client_secret", self.client_secret.as_str()),
])
.send()
.await?;
match response.status().is_success() {
true => {
let token_response: BankTokenResponse = response.json().await?;
Ok(StoredToken {
access_token: token_response.access_token,
expires_at: Utc::now() + Duration::seconds(token_response.expires_in),
})
},
false => {
let status = response.status().as_u16();
let body = response.text().await.unwrap_or_default();
Err(BankingError::ApiError { status, body })
}
}
}
/// Gets a valid token, refreshing if necessary. (Refactored)
pub async fn get_token(&self) -> Result<String, BankingError> {
// First, perform a read-only check.
let read_guard = self.token.read().await;
if let Some(token) = read_guard.as_ref() {
if token.is_valid() {
return Ok(token.access_token.clone());
}
}
// Drop the read lock before acquiring a write lock.
drop(read_guard);
// If the token is invalid or missing, acquire a write lock.
let mut write_guard = self.token.write().await;
// Re-check in case another thread refreshed the token while we were waiting.
match write_guard.as_ref() {
Some(token) if token.is_valid() => {
return Ok(token.access_token.clone())
},
_ => {
// The token is definitely invalid, so we fetch a new one.
let new_token = self.fetch_new_token().await?;
let access_token = new_token.access_token.clone();
*write_guard = Some(new_token); // Update the stored token
Ok(access_token)
}
}
}
}
impl BankingClients {
pub async fn new() -> Self {
// ...
pub async fn new() -> Result<Self, BankingError> {
let http_client = HttpClient::new();
let get_env = |key: &str| -> Result<String, BankingError> {
env::var(key).map_err(|_| BankingError::MissingEnvVar(key.to_string()))
};
let vbank = {
let base_url = Url::parse(&get_env("VBANK_API_URL")?)?;
let client_id = get_env("VBANK_CLIENT_ID")?;
let client_secret = get_env("VBANK_CLIENT_SECRET")?;
Arc::new(BankClient::new(http_client.clone(), base_url, client_id, client_secret))
};
let abank = {
let base_url = Url::parse(&get_env("ABANK_API_URL")?)?;
let client_id = get_env("ABANK_CLIENT_ID")?;
let client_secret = get_env("ABANK_CLIENT_SECRET")?;
Arc::new(BankClient::new(http_client.clone(), base_url, client_id, client_secret))
};
let sbank = {
let base_url = Url::parse(&get_env("SBANK_API_URL")?)?;
let client_id = get_env("SBANK_CLIENT_ID")?;
let client_secret = get_env("SBANK_CLIENT_SECRET")?;
Arc::new(BankClient::new(http_client, base_url, client_id, client_secret))
};
Ok(Self { vbank, abank, sbank })
}
pub fn get_client(&self, bank: Bank) -> &Arc<BankClient> {
match bank {
Bank::VBank => &self.vbank,
Bank::ABank => &self.abank,
Bank::SBank => &self.sbank,
}
}
}

View file

@ -5,8 +5,7 @@ mod state;
mod api;
use std::net::SocketAddr;
use api::banking::BankingClients;
use crate::state::AppState;
/// Общее состояние приложения, передаётся во все handlers

View file

@ -15,7 +15,7 @@ pub fn router(app_state: AppState) -> Router {
Router::new()
// GET /api/health — health-check эндпоинт
.route("/api/health", get(handlers::health_handler))
.route("/api/test-token", get(handlers::test_token_handler))
// Сюда добавим новые роуты по мере разработки:
// .route("/api/accounts", get(handlers::accounts::get_accounts))
// .route("/api/payments", post(handlers::payments::create_payment))

View file

@ -38,3 +38,15 @@ pub async fn health_handler(
).into_response(),
}
}
pub async fn test_token_handler(
State(state): State<AppState>,
) -> impl IntoResponse {
match state.banking_clients.vbank.get_token().await {
Ok(token) => (StatusCode::OK, Json(json!({ "vbank_token": token }))),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({ "error": e.to_string() })),
),
}
}

View file

@ -1,8 +1,7 @@
// src/state.rs
// Ответственность: создание и управление общим состоянием приложения (AppState)
use crate::db;
use crate::api::banking::BankingClients;
use crate::api::BankingClients;
use sqlx::PgPool;
/// Общее состояние приложения, доступное во всех handlers
@ -13,15 +12,12 @@ pub struct AppState {
}
impl AppState {
/// Асинхронный конструктор для AppState
///
/// Инициализирует все необходимые ресурсы (пул БД, HTTP клиенты)
/// и собирает их в единый стейт.
pub async fn new() -> Self {
let db_pool = db::init_pool().await;
println!("✅ Database connection pool created successfully.");
let banking_clients = BankingClients::new().await;
// FIX: Add .await and handle the potential error
let banking_clients = BankingClients::new().await.expect("Failed to initialize banking clients");
println!("✅ Banking API clients initialized.");
Self {