(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",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ edition = "2024"
|
||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
tokio = { version = "1.48", features = ["full"] }
|
tokio = { version = "1.48", features = ["full"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
url = "2.5"
|
||||||
|
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
// src/api.rs
|
// 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
|
// src/api/banking.rs
|
||||||
|
|
||||||
use reqwest::Client as HttpClient;
|
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/
|
// --- Public Enums and Errors ---
|
||||||
// Например, api/banking_models.rs
|
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub struct BankToken {
|
pub enum BankingError {
|
||||||
pub access_token: String,
|
#[error("Environment variable not set: {0}")]
|
||||||
pub expires_in: i64,
|
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 {
|
pub enum Bank {
|
||||||
VBank,
|
VBank,
|
||||||
ABank,
|
ABank,
|
||||||
SBank,
|
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)]
|
#[derive(Clone)]
|
||||||
pub struct BankClient {
|
pub struct BankClient {
|
||||||
// ...
|
http_client: HttpClient,
|
||||||
}
|
base_url: Url,
|
||||||
|
client_id: String,
|
||||||
impl BankClient {
|
client_secret: String,
|
||||||
// ...
|
// FIX: Wrap the RwLock in an Arc to make it shareable and clonable.
|
||||||
|
token: Arc<RwLock<Option<StoredToken>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BankingClients {
|
pub struct BankingClients {
|
||||||
pub vbank: BankClient,
|
pub vbank: Arc<BankClient>,
|
||||||
pub abank: BankClient,
|
pub abank: Arc<BankClient>,
|
||||||
pub sbank: 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 {
|
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;
|
mod api;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use api::banking::BankingClients;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Общее состояние приложения, передаётся во все handlers
|
/// Общее состояние приложения, передаётся во все handlers
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ pub fn router(app_state: AppState) -> Router {
|
||||||
Router::new()
|
Router::new()
|
||||||
// GET /api/health — health-check эндпоинт
|
// GET /api/health — health-check эндпоинт
|
||||||
.route("/api/health", get(handlers::health_handler))
|
.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/accounts", get(handlers::accounts::get_accounts))
|
||||||
// .route("/api/payments", post(handlers::payments::create_payment))
|
// .route("/api/payments", post(handlers::payments::create_payment))
|
||||||
|
|
|
||||||
|
|
@ -38,3 +38,15 @@ pub async fn health_handler(
|
||||||
).into_response(),
|
).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
|
// src/state.rs
|
||||||
// Ответственность: создание и управление общим состоянием приложения (AppState)
|
|
||||||
|
|
||||||
use crate::db;
|
use crate::db;
|
||||||
use crate::api::banking::BankingClients;
|
use crate::api::BankingClients;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
|
|
||||||
/// Общее состояние приложения, доступное во всех handlers
|
/// Общее состояние приложения, доступное во всех handlers
|
||||||
|
|
@ -13,15 +12,12 @@ pub struct AppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
/// Асинхронный конструктор для AppState
|
|
||||||
///
|
|
||||||
/// Инициализирует все необходимые ресурсы (пул БД, HTTP клиенты)
|
|
||||||
/// и собирает их в единый стейт.
|
|
||||||
pub async fn new() -> Self {
|
pub async fn new() -> Self {
|
||||||
let db_pool = db::init_pool().await;
|
let db_pool = db::init_pool().await;
|
||||||
println!("✅ Database connection pool created successfully.");
|
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.");
|
println!("✅ Banking API clients initialized.");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue