diff --git a/Cargo.toml b/Cargo.toml index 5ec6a17..d7dd7ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,8 @@ axum = "0.8" tokio = { version = "1.48", features = ["full"] } reqwest = { version = "0.12", features = ["json"] } url = "2.5" -argon2 = "0.5" # Password hashing -axum-extra = { version = "0.12", features = ["typed-header"] } # For Authorization +argon2 = "0.5" +axum-extra = { version = "0.12", features = ["typed-header"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros", "chrono"] } dotenvy = "0.15" @@ -34,4 +34,4 @@ anyhow = "1.0" once_cell = "1.21" [dev-dependencies] -mockito = "1.0" # Мокирование HTTP запросов +mockito = "1.0" diff --git a/secrets.yaml b/secrets.yaml index 0ff44fe..35ab5bc 100644 --- a/secrets.yaml +++ b/secrets.yaml @@ -11,6 +11,7 @@ ABANK_CLIENT_SECRET: ENC[AES256_GCM,data:qxoM8nvsmgAUKwWvOXWzwx2AhXWxj/1G1ZGwmRs SBANK_API_URL: ENC[AES256_GCM,data:1O0G8rVSqYtqfgojWgNiQsrFPln8pwcPrt87cY8YIcs=,iv:moFMGpfDbkR+W+E/iAewYlTpnUr0O/Kjoku+xeic6VI=,tag:xLHzPi7RDuOEgnglGNEBFA==,type:str] SBANK_CLIENT_ID: ENC[AES256_GCM,data:XnwwKLKUDw==,iv:Ulr1B9W9soLA78AJc1ac8bCWUSocJqjcIx5TZFm0tXQ=,tag:932t+EF78thw5hWYWJF/7w==,type:str] SBANK_CLIENT_SECRET: ENC[AES256_GCM,data:wUVzs3080UbLUAQY3jkO3O2XHeGWbeN/K8x925Rqmj4=,iv:Q6kT5qtmxY0j1a+fHn4hNvMw0m7eUEJg0Rfk8JLdVts=,tag:Yx2kfHyA3d9iheS9pe+s6g==,type:str] +JWT_SECRET: ENC[AES256_GCM,data:9kt3PFNonazjrODAV3P7YCf1USGdqG/G,iv:Z5R+mZuVXm01CTvNKOEyO1zsalQOv2YjClMYOcNt8Ic=,tag:PgWbVVA/FLN+2FPR0btwEg==,type:str] sops: age: - recipient: age1hg8s2z00r8d76qfgea8cde3dakz2eguvtmlzavl8nwmh8ueany0suvdej4 @@ -40,7 +41,7 @@ sops: Vm5FSnlGYXFGT1g3WUh3UWZDcGtNZEkKMSndRqxLO7/d5GI3oOGi65J5gkMOlIOf dSqB2eoVvr0b8bUMEBuW4JAET9xAG64VR9QNt4QlSeebWLjJ/XAenw== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-11-05T14:40:11Z" - mac: ENC[AES256_GCM,data:ZZqvGGdWJMarG0vG2nEneb2klGF9+qBg22/Anp83md0dZBWHDxfMDh4SLNdNCUSnBWFXBMuDcDWJVfT3QhMS1pmk64iSiF1y9TrZWCrkX2BqJGU7Xp4xuACa/s5JT3w6o1hmhShtp463Jxu9PMkHt5rG9yc4uakOObzaJEYAM0g=,iv:0Fy6fHjLrLK9ATImv7m734JVL1gLnhFVL4nrLNzadd0=,tag:fWovzNSGt3ujOjmwYYHBKQ==,type:str] + lastmodified: "2025-11-09T12:53:08Z" + mac: ENC[AES256_GCM,data:H9mglULcgwRvUXpJKbLRAf4wx3/n/8Yf/AtcN7DHajd16Poh7W8nzsDpwRmyItrcnDrZSUZo35HsbLCGOmQHMAS3X4BfD/c0Nd+cWsHvcZjCSuTbFo5+B8BtKvDN8TW+y3GA+ly5hnsNJKI6q1v1JJHgOIOUxWUTAUb6LjqH7mo=,iv:3HlRtBEB6RF+an1hiaDND182uu1HtL1T803gdseSVGg=,tag:46soOX4gQTIH6efH3xKCzg==,type:str] unencrypted_suffix: _unencrypted version: 3.11.0 diff --git a/src/api.rs b/src/api.rs index 959de36..59068d9 100644 --- a/src/api.rs +++ b/src/api.rs @@ -7,4 +7,3 @@ pub mod accounts; pub mod transactions; pub use client::{BankingError, BankingClients}; -// pub use models::{ApiResponse, AccountData, TransactionData, ConsentResponse}; diff --git a/src/api/accounts.rs b/src/api/accounts.rs index dbedf0a..f545cd8 100644 --- a/src/api/accounts.rs +++ b/src/api/accounts.rs @@ -1,5 +1,4 @@ // src/api/accounts.rs -// Account data retrieval use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData, BalanceResponse}}; use tracing::{info, error, debug}; @@ -58,7 +57,7 @@ impl BankClient { &self, account_id: &str, consent_id: &str, - ) -> Result { // ← Changed return type! + ) -> Result { info!("📊 Fetching balances for account: {}", account_id); let token = self.get_token().await?; @@ -83,7 +82,7 @@ impl BankClient { let text = response.text().await?; info!("✅ Balance response received"); - serde_json::from_str::(&text) // ← Parse as BalanceResponse + serde_json::from_str::(&text) .map_err(|e| { error!("❌ Failed to deserialize BalanceResponse: {}", e); error!("Raw: {}", text); diff --git a/src/api/client.rs b/src/api/client.rs index 9c1f62a..be4ec52 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -1,5 +1,4 @@ // src/api/client.rs -// Core banking API client implementation use reqwest::Client as HttpClient; use std::{env, str::FromStr, sync::Arc}; @@ -128,7 +127,6 @@ impl BankClient { pub async fn get_token(&self) -> Result { - // Check if we have a valid token with a read lock { let read_guard = self.token.read().await; if let Some(token) = read_guard.as_ref().filter(|t| t.is_valid()) { @@ -136,10 +134,8 @@ impl BankClient { } } - // Acquire write lock and refresh if needed let mut write_guard = self.token.write().await; - - // Double-check in case another task refreshed while we waited + match write_guard.as_ref() { Some(token) if token.is_valid() => Ok(token.access_token.clone()), _ => { diff --git a/src/api/consents.rs b/src/api/consents.rs index 37ec845..d3e458b 100644 --- a/src/api/consents.rs +++ b/src/api/consents.rs @@ -1,5 +1,4 @@ // src/api/consents.rs -// Consent request and retrieval logic - Functional style use super::{client::{BankClient, BankingError}, models::{ConsentRequestBody, ConsentResponse}}; use tracing::{info, debug, error}; diff --git a/src/api/models.rs b/src/api/models.rs index e928374..8507a7c 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -1,19 +1,17 @@ // src/api/models.rs -// This file contains all the data structures for the banking API. -// These are the "nouns" of our API interaction. use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc, NaiveDate}; // --- Generic API Wrappers --- -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct ApiResponse { pub data: T, pub links: Links, pub meta: Meta, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Links { #[serde(rename = "self")] @@ -22,7 +20,7 @@ pub struct Links { pub prev: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Meta { pub total_pages: Option, @@ -42,7 +40,7 @@ pub struct ConsentRequestBody { pub requesting_bank_name: String, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "snake_case")] pub struct ConsentResponse { pub request_id: String, @@ -54,12 +52,12 @@ pub struct ConsentResponse { } // --- Account & Transaction Models --- -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct AccountData { pub account: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Account { pub account_id: String, @@ -73,7 +71,7 @@ pub struct Account { pub account: Option>, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct AccountIdentification { pub scheme_name: String, @@ -86,12 +84,12 @@ pub struct BalanceResponse { pub data: BalanceData, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct BalanceData { pub balance: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Balance { pub account_id: String, @@ -102,13 +100,13 @@ pub struct Balance { pub credit_debit_indicator: String, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct TransactionData { pub transaction: Vec, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(rename_all = "camelCase")] pub struct Transaction { pub account_id: String, @@ -122,13 +120,13 @@ pub struct Transaction { pub bank_transaction_code: Option, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Amount { pub amount: String, pub currency: String, } -#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct BankTransactionCode { pub code: String, } diff --git a/src/api/transactions.rs b/src/api/transactions.rs index ebc5e12..35df4bd 100644 --- a/src/api/transactions.rs +++ b/src/api/transactions.rs @@ -1,5 +1,4 @@ // src/api/transactions.rs -// Transaction data retrieval with pagination support use super::{client::{BankClient, BankingError}, models::{ApiResponse, TransactionData}}; diff --git a/src/auth/handlers.rs b/src/auth/handlers.rs index 947a754..1a0635f 100644 --- a/src/auth/handlers.rs +++ b/src/auth/handlers.rs @@ -19,13 +19,13 @@ use super::{jwt::generate_token, password::{hash_password, verify_password}}; #[derive(Debug, Deserialize)] pub struct RegisterRequest { - pub bank_user_number: u8, // 1-10: User picks their slot (e.g., 6 → team275-6) - pub password: String, // User-created password + pub bank_user_number: u8, + pub password: String, } #[derive(Debug, Deserialize)] pub struct LoginRequest { - pub bank_user_id: String, // e.g., "team275-6" + pub bank_user_id: String, pub password: String, } @@ -39,13 +39,13 @@ pub async fn register_handler( State(state): State, Json(payload): Json, ) -> impl IntoResponse { - info!("🔐 REGISTER REQUEST"); // ← ADD - debug!(" bank_user_number: {}", payload.bank_user_number); // ← ADD - debug!(" password length: {} chars", payload.password.len()); // ← ADD + info!("🔐 REGISTER REQUEST"); + debug!(" bank_user_number: {}", payload.bank_user_number); + debug!(" password length: {} chars", payload.password.len()); // Validate password length if payload.password.len() < 3 { - error!("❌ Password too short"); // ← ADD + error!("❌ Password too short"); return Err(( StatusCode::BAD_REQUEST, Json(json!({ "error": "Password must be at least 3 characters" })) @@ -54,7 +54,7 @@ pub async fn register_handler( // Validate bank_user_number (1-10) if payload.bank_user_number < 1 || payload.bank_user_number > 10 { - error!("❌ Invalid bank_user_number: {}", payload.bank_user_number); // ← ADD + error!("❌ Invalid bank_user_number: {}", payload.bank_user_number); return Err(( StatusCode::BAD_REQUEST, Json(json!({ "error": "bank_user_number must be between 1 and 10" })) @@ -64,44 +64,44 @@ pub async fn register_handler( // Hash password let password_hash = hash_password(&payload.password) .map_err(|e| { - error!("❌ Password hashing failed: {}", e); // ← ADD + error!("❌ Password hashing failed: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "Failed to hash password" })) ) })?; - debug!("✅ Password hashed successfully"); // ← ADD + debug!("✅ Password hashed successfully"); // Build bank_user_id from environment + number let bank_user_id = format!("{}-{}", state.bank_team_id, payload.bank_user_number); - info!(" Generated bank_user_id: {}", bank_user_id); // ← ADD + info!(" Generated bank_user_id: {}", bank_user_id); // Create user in database let user = db::users::create_user(&state.db_pool, &bank_user_id, &password_hash) .await .map_err(|e| { - error!("❌ Database error during user creation: {}", e); // ← ADD + error!("❌ Database error during user creation: {}", e); ( StatusCode::CONFLICT, Json(json!({ "error": format!("User already exists or database error: {}", e) })) ) })?; - info!("✅ User created in database (ID: {})", user.id); // ← ADD + info!("✅ User created in database (ID: {})", user.id); // Generate JWT token let token = generate_token(user.id, &user.bank_user_id) .map_err(|e| { - error!("❌ JWT generation failed: {}", e); // ← ADD + error!("❌ JWT generation failed: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "Failed to generate token" })) ) })?; - info!("✅ JWT token generated"); // ← ADD - info!("🎉 REGISTER SUCCESSFUL for user: {}", bank_user_id); // ← ADD + info!("✅ JWT token generated"); + info!("🎉 REGISTER SUCCESSFUL for user: {}", bank_user_id); Ok::<_, (StatusCode, Json)>(( StatusCode::CREATED, @@ -116,22 +116,22 @@ pub async fn login_handler( State(state): State, Json(payload): Json, ) -> impl IntoResponse { - info!("🔑 LOGIN REQUEST"); // ← ADD - debug!(" bank_user_id: {}", payload.bank_user_id); // ← ADD - debug!(" password length: {} chars", payload.password.len()); // ← ADD + info!("🔑 LOGIN REQUEST"); + debug!(" bank_user_id: {}", payload.bank_user_id); + debug!(" password length: {} chars", payload.password.len()); // Look up user by bank_user_id let user_data = db::users::get_user_by_bank_user_id(&state.db_pool, &payload.bank_user_id) .await .map_err(|e| { - error!("❌ Database error: {}", e); // ← ADD + error!("❌ Database error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "Database error" })) ) })? .ok_or_else(|| { - warn!("⚠️ User not found: {}", payload.bank_user_id); // ← ADD + warn!("⚠️ User not found: {}", payload.bank_user_id); ( StatusCode::UNAUTHORIZED, Json(json!({ "error": "Invalid bank_user_id or password" })) @@ -139,12 +139,12 @@ pub async fn login_handler( })?; let (user_id, password_hash) = user_data; - info!("✅ User found in database (ID: {})", user_id); // ← ADD + info!("✅ User found in database (ID: {})", user_id); // Verify password let is_valid = verify_password(&payload.password, &password_hash) .map_err(|e| { - error!("❌ Password verification error: {}", e); // ← ADD + error!("❌ Password verification error: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "Password verification failed" })) @@ -152,27 +152,27 @@ pub async fn login_handler( })?; if !is_valid { - warn!("⚠️ Invalid password for user: {}", payload.bank_user_id); // ← ADD + warn!("⚠️ Invalid password for user: {}", payload.bank_user_id); return Err(( StatusCode::UNAUTHORIZED, Json(json!({ "error": "Invalid bank_user_id or password" })) )); } - info!("✅ Password verified"); // ← ADD + info!("✅ Password verified"); // Generate JWT token let token = generate_token(user_id, &payload.bank_user_id) .map_err(|e| { - error!("❌ JWT generation failed: {}", e); // ← ADD + error!("❌ JWT generation failed: {}", e); ( StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": "Failed to generate token" })) ) })?; - info!("✅ JWT token generated"); // ← ADD - info!("🎉 LOGIN SUCCESSFUL for user: {}", payload.bank_user_id); // ← ADD + info!("✅ JWT token generated"); + info!("🎉 LOGIN SUCCESSFUL for user: {}", payload.bank_user_id); Ok::<_, (StatusCode, Json)>(( StatusCode::OK, @@ -186,7 +186,7 @@ pub async fn login_handler( pub async fn me_handler( axum::extract::Extension(claims): axum::extract::Extension, ) -> impl IntoResponse { - info!("👤 GET ME REQUEST from user: {}", claims.bank_user_id); // ← ADD + info!("👤 GET ME REQUEST from user: {}", claims.bank_user_id); ( StatusCode::OK, diff --git a/src/auth/jwt.rs b/src/auth/jwt.rs index e6e3c62..f8db94a 100644 --- a/src/auth/jwt.rs +++ b/src/auth/jwt.rs @@ -1,6 +1,4 @@ // src/auth/jwt.rs -// JWT token generation and validation -// Token contains bank_user_id so backend knows which Multiberry user is calling use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; @@ -9,9 +7,9 @@ use std::env; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { - pub sub: i32, // Internal user_id from database - pub bank_user_id: String, // e.g., "team275-6" - identifies which Multiberry user - pub exp: i64, // Expiration timestamp + pub sub: i32, + pub bank_user_id: String, + pub exp: i64, } pub fn generate_token(user_id: i32, bank_user_id: &str) -> Result { diff --git a/src/auth/middleware.rs b/src/auth/middleware.rs index e87dfc8..2787762 100644 --- a/src/auth/middleware.rs +++ b/src/auth/middleware.rs @@ -1,5 +1,4 @@ // src/auth/middleware.rs -// Axum middleware to protect routes with JWT authentication use axum::{ body::Body, @@ -9,6 +8,7 @@ use axum::{ response::Response, Json, }; +use tracing::{info, error}; use serde_json::json; use super::jwt::validate_token; @@ -31,6 +31,7 @@ pub async fn auth_middleware( StatusCode::UNAUTHORIZED, Json(json!({ "error": "Invalid or expired token" })) ))?; + info!("🔑auth_middleware token: {}", token); req.extensions_mut().insert(claims); diff --git a/src/auth/password.rs b/src/auth/password.rs index ba18a52..81e0797 100644 --- a/src/auth/password.rs +++ b/src/auth/password.rs @@ -1,5 +1,4 @@ // src/auth/password.rs -// Password hashing and verification using Argon2 use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, diff --git a/src/db/accounts.rs b/src/db/accounts.rs index 5891856..e366d6e 100644 --- a/src/db/accounts.rs +++ b/src/db/accounts.rs @@ -6,8 +6,8 @@ use chrono::NaiveDate; pub async fn store_account( pool: &PgPool, account_id: &str, - user_id: &str, // team275-6 - bank_code: &str, // vbank + user_id: &str, + bank_code: &str, status: &str, currency: &str, account_type: &str, @@ -46,7 +46,7 @@ pub async fn get_accounts_for_user( pool: &PgPool, user_id: &str, bank_code: &str, -) -> Result, sqlx::Error> { // Returns (account_id, nickname) +) -> Result, sqlx::Error> { sqlx::query!( r#" SELECT account_id, nickname diff --git a/src/main.rs b/src/main.rs index 8088ffd..b6880e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod api; mod auth; mod db; mod route; -mod state; // ← Already have this +mod state; use state::AppState; @@ -16,7 +16,6 @@ use state::AppState; async fn main() { dotenvy::dotenv().ok(); - // Initialize tracing with detailed logging tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .with_target(true) @@ -27,15 +26,14 @@ async fn main() { tracing::info!("🚀 Starting Multiberry Backend Server"); let app_state = AppState::new().await; - - // ✨ Add CORS layer + let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let app = route::router(app_state) - .layer(cors) // ← Add CORS before TraceLayer + .layer(cors) .layer( TraceLayer::new_for_http() .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) diff --git a/src/route.rs b/src/route.rs index 57f43c5..f51e7d1 100644 --- a/src/route.rs +++ b/src/route.rs @@ -12,35 +12,34 @@ use crate::{auth, state::AppState}; pub mod handlers; pub fn router(app_state: AppState) -> Router { - // Public routes (no auth required) + let public_routes = Router::new() .route("/api/health", get(handlers::health_handler)) .route("/api/auth/register", post(auth::handlers::register_handler)) .route("/api/auth/login", post(auth::handlers::login_handler)); - // Protected routes (auth required) - user_id extracted from JWT in handlers + let protected_routes = Router::new() .route("/api/auth/me", get(auth::handlers::me_handler)) - .route("/api/consent/{bank}", // ← Removed {user_id} - get from JWT! + .route("/api/consent/{bank}", post(handlers::create_consent_handler) .get(handlers::get_consent_handler) .delete(handlers::delete_consent_handler) ) - .route("/api/accounts/{bank}", // ← Removed {user_id} - get from JWT! + .route("/api/accounts/{bank}", get(handlers::get_accounts_handler) ) - .route("/api/transactions/{bank}/{account_id}", // ← Removed {user_id} + .route("/api/transactions/{bank}/{account_id}", get(handlers::get_transactions_handler) ) - .route("/api/transactions", // ← Get all transactions (user from JWT) + .route("/api/transactions", get(handlers::get_all_transactions_handler) ) .route("/api/balances/{bank}/{account_id}", get(handlers::get_balances_handler) ) - .layer(middleware::from_fn(auth::auth_middleware)); // ← Your existing middleware - - // Merge both + .layer(middleware::from_fn(auth::auth_middleware)); + public_routes .merge(protected_routes) .with_state(app_state) diff --git a/src/route/handlers.rs b/src/route/handlers.rs index c7c3dea..722fcf9 100644 --- a/src/route/handlers.rs +++ b/src/route/handlers.rs @@ -1,5 +1,4 @@ // src/route/handlers.rs -// HTTP request handlers for the banking API use axum::{ extract::{Path, Query, State}, @@ -50,21 +49,22 @@ struct ConsentCreatedResponse { pub async fn create_consent_handler( State(state): State, - Path(bank_code): Path, // ← Only bank_code now - Extension(claims): Extension, // ← Get user_id from JWT! + Path(bank_code): Path, + Extension(claims): Extension, ) -> impl IntoResponse { - let user_id = &claims.bank_user_id; // ← Extract from JWT + let user_id = &claims.bank_user_id; let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code. Use: vbank, abank, or sbank" }))))?; - // Rest stays the same - just use `user_id` variable + info!("🤝 create_consent for user_id: {} bank: {}", user_id, bank.code()); + let client = state.banking_clients.get_client(bank); let consent_response = client - .request_consent(user_id) // ← Use extracted user_id + .request_consent(user_id) .await .map_err(map_banking_error)?; @@ -76,7 +76,7 @@ pub async fn create_consent_handler( db::consents::store_consent( &state.db_pool, - user_id, // ← Use extracted user_id + user_id, bank.code(), &consent_response.consent_id, expires_at, @@ -100,16 +100,18 @@ pub async fn create_consent_handler( pub async fn get_consent_handler( State(state): State, - Path(bank_code): Path, // ← Only bank_code now - Extension(claims): Extension, // ← Get user_id from JWT! + Path(bank_code): Path, + Extension(claims): Extension, ) -> impl IntoResponse { - let user_id = &claims.bank_user_id; // ← Extract from JWT + let user_id = &claims.bank_user_id; let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?; + info!("🤝 get_consent for user_id: {} bank: {}", user_id, bank.code()); + db::consents::get_valid_consent(&state.db_pool, user_id, bank.code()) .await .map_err(|e| ( @@ -130,10 +132,10 @@ pub async fn get_consent_handler( pub async fn get_accounts_handler( State(state): State, - Path(bank_code): Path, // ← Only bank_code now - Extension(claims): Extension, // ← Get user_id from JWT! + Path(bank_code): Path, + Extension(claims): Extension, ) -> Result, (StatusCode, Json)> { - let user_id = &claims.bank_user_id; // ← Extract from JWT + let user_id = &claims.bank_user_id; let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?; @@ -146,18 +148,20 @@ pub async fn get_accounts_handler( Json(json!({ "error": "No valid consent. Please request consent first." })) ))?; + info!("💳 get_accounts for user_id: {} bank: {}", user_id, bank.code()); + let client = state.banking_clients.get_client(bank); let accounts_response = client.get_accounts(user_id, &consent_id) .await .map_err(map_banking_error)?; - // ✨ Save accounts to database + // Save accounts to database for account in &accounts_response.data.account { let _ = db::accounts::store_account( &state.db_pool, &account.account_id, - user_id, // ← Use extracted user_id + user_id, bank.code(), account.status.as_deref().unwrap_or("unknown"), &account.currency, @@ -183,15 +187,17 @@ pub struct TransactionQuery { pub async fn get_transactions_handler( State(state): State, - Path((bank_code, account_id)): Path<(String, String)>, // ← Changed: no user_id + Path((bank_code, account_id)): Path<(String, String)>, Query(params): Query, - Extension(claims): Extension, // ← Get user_id from JWT! + Extension(claims): Extension, ) -> Result, (StatusCode, Json)> { - let user_id = &claims.bank_user_id; // ← Extract from JWT + let user_id = &claims.bank_user_id; + let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?; + info!("📊 get_transactions account_id: {} bank: {}", account_id, bank.code()); let consent_id = db::consents::get_valid_consent(&state.db_pool, user_id, bank.code()) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))? @@ -206,7 +212,7 @@ pub async fn get_transactions_handler( .await .map_err(map_banking_error)?; - // ✨ Save all transactions to cache + // Save all transactions to cache for tx in &transactions_response.data.transaction { let _ = db::transactions::store_transaction( &state.db_pool, @@ -230,15 +236,14 @@ pub async fn get_transactions_handler( pub async fn get_all_transactions_handler( State(state): State, Query(params): Query, - Extension(claims): Extension, // ← Add this + Extension(claims): Extension, ) -> Result, (StatusCode, Json)> { let bank_user_id = &claims.bank_user_id; let page = params.page.unwrap_or(1); - let limit = params.limit.unwrap_or(20); // Default: 20 на странице для ленты + let limit = params.limit.unwrap_or(20); - info!("📊 Fetching ALL transactions page {} (limit {})", page, limit); + info!("📊 get_all_transactions page {} (limit {})", page, limit); - // Validate if limit > 100 { return Err(( StatusCode::BAD_REQUEST, @@ -248,7 +253,6 @@ pub async fn get_all_transactions_handler( let offset = (page - 1) * limit; - // Get total count let total_count: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM transactions WHERE account_id IN ( SELECT account_id FROM accounts WHERE user_id = $1 @@ -259,7 +263,6 @@ pub async fn get_all_transactions_handler( .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?; - // Get paginated transactions let transactions = sqlx::query!( r#" SELECT @@ -316,27 +319,27 @@ pub async fn get_all_transactions_handler( pub async fn delete_consent_handler( State(state): State, - Path(bank_code): Path, // ← Only bank_code now - Extension(claims): Extension, // ← Get user_id from JWT! + Path(bank_code): Path, + Extension(claims): Extension, ) -> impl IntoResponse { - let user_id = &claims.bank_user_id; // ← Extract from JWT + let user_id = &claims.bank_user_id; let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?; + info!("🗑 delete_consent for user_id: {} bank: {}", user_id, bank.code()); + let consent_id = db::consents::get_valid_consent(&state.db_pool, user_id, bank.code()) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))? .ok_or_else(|| (StatusCode::NOT_FOUND, Json(json!({ "error": "Consent not found" }))))?; let client = state.banking_clients.get_client(bank); - - // Delete from bank first + client.delete_consent(&consent_id) .await .map_err(map_banking_error)?; - // Then delete from our DB db::consents::delete_consent(&state.db_pool, &consent_id) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?; @@ -346,13 +349,13 @@ pub async fn delete_consent_handler( pub async fn get_balances_handler( State(state): State, - Path((bank_code, account_id)): Path<(String, String)>, // ← account_id, not user_id! + Path((bank_code, account_id)): Path<(String, String)>, ) -> Result, (StatusCode, Json)> { let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?; - // We need to get consent from the account's user - // For now, just get the first valid consent for this bank + info!("💰 get_balances account_id: {} bank: {}", account_id, bank.code()); + let consent_id = db::consents::get_first_valid_consent(&state.db_pool, bank.code()) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))? @@ -360,11 +363,8 @@ pub async fn get_balances_handler( let client = state.banking_clients.get_client(bank); - info!("📊 Fetching balances for account: {} from bank: {}", account_id, bank.code()); - let response = client.get_balances(&account_id, &consent_id).await.map_err(map_banking_error)?; - // ✨ Cache balances for balance in &response.data.balance { let _ = db::balances::store_balance( &state.db_pool, diff --git a/src/state.rs b/src/state.rs index 4871cf4..d048072 100644 --- a/src/state.rs +++ b/src/state.rs @@ -5,12 +5,12 @@ use crate::api::BankingClients; use sqlx::PgPool; use std::env; -/// Global application state, accessible in all handlers + #[derive(Clone)] pub struct AppState { pub db_pool: PgPool, pub banking_clients: BankingClients, - pub bank_team_id: String, // e.g., "team275" from VBANK_CLIENT_ID env var + pub bank_team_id: String, } impl AppState {