(feat) added handler
This commit is contained in:
parent
5b3c2d4ec7
commit
de7c0f7110
8 changed files with 203 additions and 32 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1227,6 +1227,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ mod state;
|
|||
mod api;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use api::banking::BankingClients;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
|
||||
/// Общее состояние приложения, передаётся во все handlers
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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() })),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
src/state.rs
10
src/state.rs
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue