finish 30% lol
This commit is contained in:
parent
4d0ad0226f
commit
ea863ad124
17 changed files with 109 additions and 123 deletions
|
|
@ -8,8 +8,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"
|
url = "2.5"
|
||||||
argon2 = "0.5" # Password hashing
|
argon2 = "0.5"
|
||||||
axum-extra = { version = "0.12", features = ["typed-header"] } # For Authorization
|
axum-extra = { version = "0.12", features = ["typed-header"] }
|
||||||
|
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros", "chrono"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "macros", "chrono"] }
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
|
|
@ -34,4 +34,4 @@ anyhow = "1.0"
|
||||||
once_cell = "1.21"
|
once_cell = "1.21"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockito = "1.0" # Мокирование HTTP запросов
|
mockito = "1.0"
|
||||||
|
|
|
||||||
|
|
@ -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_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_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]
|
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:
|
sops:
|
||||||
age:
|
age:
|
||||||
- recipient: age1hg8s2z00r8d76qfgea8cde3dakz2eguvtmlzavl8nwmh8ueany0suvdej4
|
- recipient: age1hg8s2z00r8d76qfgea8cde3dakz2eguvtmlzavl8nwmh8ueany0suvdej4
|
||||||
|
|
@ -40,7 +41,7 @@ sops:
|
||||||
Vm5FSnlGYXFGT1g3WUh3UWZDcGtNZEkKMSndRqxLO7/d5GI3oOGi65J5gkMOlIOf
|
Vm5FSnlGYXFGT1g3WUh3UWZDcGtNZEkKMSndRqxLO7/d5GI3oOGi65J5gkMOlIOf
|
||||||
dSqB2eoVvr0b8bUMEBuW4JAET9xAG64VR9QNt4QlSeebWLjJ/XAenw==
|
dSqB2eoVvr0b8bUMEBuW4JAET9xAG64VR9QNt4QlSeebWLjJ/XAenw==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2025-11-05T14:40:11Z"
|
lastmodified: "2025-11-09T12:53:08Z"
|
||||||
mac: ENC[AES256_GCM,data:ZZqvGGdWJMarG0vG2nEneb2klGF9+qBg22/Anp83md0dZBWHDxfMDh4SLNdNCUSnBWFXBMuDcDWJVfT3QhMS1pmk64iSiF1y9TrZWCrkX2BqJGU7Xp4xuACa/s5JT3w6o1hmhShtp463Jxu9PMkHt5rG9yc4uakOObzaJEYAM0g=,iv:0Fy6fHjLrLK9ATImv7m734JVL1gLnhFVL4nrLNzadd0=,tag:fWovzNSGt3ujOjmwYYHBKQ==,type:str]
|
mac: ENC[AES256_GCM,data:H9mglULcgwRvUXpJKbLRAf4wx3/n/8Yf/AtcN7DHajd16Poh7W8nzsDpwRmyItrcnDrZSUZo35HsbLCGOmQHMAS3X4BfD/c0Nd+cWsHvcZjCSuTbFo5+B8BtKvDN8TW+y3GA+ly5hnsNJKI6q1v1JJHgOIOUxWUTAUb6LjqH7mo=,iv:3HlRtBEB6RF+an1hiaDND182uu1HtL1T803gdseSVGg=,tag:46soOX4gQTIH6efH3xKCzg==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.11.0
|
version: 3.11.0
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,3 @@ pub mod accounts;
|
||||||
pub mod transactions;
|
pub mod transactions;
|
||||||
|
|
||||||
pub use client::{BankingError, BankingClients};
|
pub use client::{BankingError, BankingClients};
|
||||||
// pub use models::{ApiResponse, AccountData, TransactionData, ConsentResponse};
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/api/accounts.rs
|
// src/api/accounts.rs
|
||||||
// Account data retrieval
|
|
||||||
|
|
||||||
use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData, BalanceResponse}};
|
use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData, BalanceResponse}};
|
||||||
use tracing::{info, error, debug};
|
use tracing::{info, error, debug};
|
||||||
|
|
@ -58,7 +57,7 @@ impl BankClient {
|
||||||
&self,
|
&self,
|
||||||
account_id: &str,
|
account_id: &str,
|
||||||
consent_id: &str,
|
consent_id: &str,
|
||||||
) -> Result<BalanceResponse, BankingError> { // ← Changed return type!
|
) -> Result<BalanceResponse, BankingError> {
|
||||||
info!("📊 Fetching balances for account: {}", account_id);
|
info!("📊 Fetching balances for account: {}", account_id);
|
||||||
|
|
||||||
let token = self.get_token().await?;
|
let token = self.get_token().await?;
|
||||||
|
|
@ -83,7 +82,7 @@ impl BankClient {
|
||||||
let text = response.text().await?;
|
let text = response.text().await?;
|
||||||
info!("✅ Balance response received");
|
info!("✅ Balance response received");
|
||||||
|
|
||||||
serde_json::from_str::<BalanceResponse>(&text) // ← Parse as BalanceResponse
|
serde_json::from_str::<BalanceResponse>(&text)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ Failed to deserialize BalanceResponse: {}", e);
|
error!("❌ Failed to deserialize BalanceResponse: {}", e);
|
||||||
error!("Raw: {}", text);
|
error!("Raw: {}", text);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/api/client.rs
|
// src/api/client.rs
|
||||||
// Core banking API client implementation
|
|
||||||
|
|
||||||
use reqwest::Client as HttpClient;
|
use reqwest::Client as HttpClient;
|
||||||
use std::{env, str::FromStr, sync::Arc};
|
use std::{env, str::FromStr, sync::Arc};
|
||||||
|
|
@ -128,7 +127,6 @@ impl BankClient {
|
||||||
|
|
||||||
|
|
||||||
pub async fn get_token(&self) -> Result<String, BankingError> {
|
pub async fn get_token(&self) -> Result<String, BankingError> {
|
||||||
// Check if we have a valid token with a read lock
|
|
||||||
{
|
{
|
||||||
let read_guard = self.token.read().await;
|
let read_guard = self.token.read().await;
|
||||||
if let Some(token) = read_guard.as_ref().filter(|t| t.is_valid()) {
|
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;
|
let mut write_guard = self.token.write().await;
|
||||||
|
|
||||||
// Double-check in case another task refreshed while we waited
|
|
||||||
match write_guard.as_ref() {
|
match write_guard.as_ref() {
|
||||||
Some(token) if token.is_valid() => Ok(token.access_token.clone()),
|
Some(token) if token.is_valid() => Ok(token.access_token.clone()),
|
||||||
_ => {
|
_ => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/api/consents.rs
|
// src/api/consents.rs
|
||||||
// Consent request and retrieval logic - Functional style
|
|
||||||
|
|
||||||
use super::{client::{BankClient, BankingError}, models::{ConsentRequestBody, ConsentResponse}};
|
use super::{client::{BankClient, BankingError}, models::{ConsentRequestBody, ConsentResponse}};
|
||||||
use tracing::{info, debug, error};
|
use tracing::{info, debug, error};
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,17 @@
|
||||||
// src/api/models.rs
|
// 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 serde::{Deserialize, Serialize};
|
||||||
use chrono::{DateTime, Utc, NaiveDate};
|
use chrono::{DateTime, Utc, NaiveDate};
|
||||||
|
|
||||||
// --- Generic API Wrappers ---
|
// --- Generic API Wrappers ---
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct ApiResponse<T> {
|
pub struct ApiResponse<T> {
|
||||||
pub data: T,
|
pub data: T,
|
||||||
pub links: Links,
|
pub links: Links,
|
||||||
pub meta: Meta,
|
pub meta: Meta,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Links {
|
pub struct Links {
|
||||||
#[serde(rename = "self")]
|
#[serde(rename = "self")]
|
||||||
|
|
@ -22,7 +20,7 @@ pub struct Links {
|
||||||
pub prev: Option<String>,
|
pub prev: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Meta {
|
pub struct Meta {
|
||||||
pub total_pages: Option<i32>,
|
pub total_pages: Option<i32>,
|
||||||
|
|
@ -42,7 +40,7 @@ pub struct ConsentRequestBody {
|
||||||
pub requesting_bank_name: String,
|
pub requesting_bank_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "snake_case")]
|
#[serde(rename_all = "snake_case")]
|
||||||
pub struct ConsentResponse {
|
pub struct ConsentResponse {
|
||||||
pub request_id: String,
|
pub request_id: String,
|
||||||
|
|
@ -54,12 +52,12 @@ pub struct ConsentResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Account & Transaction Models ---
|
// --- Account & Transaction Models ---
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct AccountData {
|
pub struct AccountData {
|
||||||
pub account: Vec<Account>,
|
pub account: Vec<Account>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Account {
|
pub struct Account {
|
||||||
pub account_id: String,
|
pub account_id: String,
|
||||||
|
|
@ -73,7 +71,7 @@ pub struct Account {
|
||||||
pub account: Option<Vec<AccountIdentification>>,
|
pub account: Option<Vec<AccountIdentification>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct AccountIdentification {
|
pub struct AccountIdentification {
|
||||||
pub scheme_name: String,
|
pub scheme_name: String,
|
||||||
|
|
@ -86,12 +84,12 @@ pub struct BalanceResponse {
|
||||||
pub data: BalanceData,
|
pub data: BalanceData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct BalanceData {
|
pub struct BalanceData {
|
||||||
pub balance: Vec<Balance>,
|
pub balance: Vec<Balance>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Balance {
|
pub struct Balance {
|
||||||
pub account_id: String,
|
pub account_id: String,
|
||||||
|
|
@ -102,13 +100,13 @@ pub struct Balance {
|
||||||
pub credit_debit_indicator: String,
|
pub credit_debit_indicator: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct TransactionData {
|
pub struct TransactionData {
|
||||||
pub transaction: Vec<Transaction>,
|
pub transaction: Vec<Transaction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Transaction {
|
pub struct Transaction {
|
||||||
pub account_id: String,
|
pub account_id: String,
|
||||||
|
|
@ -122,13 +120,13 @@ pub struct Transaction {
|
||||||
pub bank_transaction_code: Option<BankTransactionCode>,
|
pub bank_transaction_code: Option<BankTransactionCode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct Amount {
|
pub struct Amount {
|
||||||
pub amount: String,
|
pub amount: String,
|
||||||
pub currency: String,
|
pub currency: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Clone)] // Added Serialize here
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
pub struct BankTransactionCode {
|
pub struct BankTransactionCode {
|
||||||
pub code: String,
|
pub code: String,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/api/transactions.rs
|
// src/api/transactions.rs
|
||||||
// Transaction data retrieval with pagination support
|
|
||||||
|
|
||||||
use super::{client::{BankClient, BankingError}, models::{ApiResponse, TransactionData}};
|
use super::{client::{BankClient, BankingError}, models::{ApiResponse, TransactionData}};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,13 +19,13 @@ use super::{jwt::generate_token, password::{hash_password, verify_password}};
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct RegisterRequest {
|
pub struct RegisterRequest {
|
||||||
pub bank_user_number: u8, // 1-10: User picks their slot (e.g., 6 → team275-6)
|
pub bank_user_number: u8,
|
||||||
pub password: String, // User-created password
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct LoginRequest {
|
pub struct LoginRequest {
|
||||||
pub bank_user_id: String, // e.g., "team275-6"
|
pub bank_user_id: String,
|
||||||
pub password: String,
|
pub password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,13 +39,13 @@ pub async fn register_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<RegisterRequest>,
|
Json(payload): Json<RegisterRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("🔐 REGISTER REQUEST"); // ← ADD
|
info!("🔐 REGISTER REQUEST");
|
||||||
debug!(" bank_user_number: {}", payload.bank_user_number); // ← ADD
|
debug!(" bank_user_number: {}", payload.bank_user_number);
|
||||||
debug!(" password length: {} chars", payload.password.len()); // ← ADD
|
debug!(" password length: {} chars", payload.password.len());
|
||||||
|
|
||||||
// Validate password length
|
// Validate password length
|
||||||
if payload.password.len() < 3 {
|
if payload.password.len() < 3 {
|
||||||
error!("❌ Password too short"); // ← ADD
|
error!("❌ Password too short");
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
Json(json!({ "error": "Password must be at least 3 characters" }))
|
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)
|
// Validate bank_user_number (1-10)
|
||||||
if payload.bank_user_number < 1 || payload.bank_user_number > 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((
|
return Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
Json(json!({ "error": "bank_user_number must be between 1 and 10" }))
|
Json(json!({ "error": "bank_user_number must be between 1 and 10" }))
|
||||||
|
|
@ -64,44 +64,44 @@ pub async fn register_handler(
|
||||||
// Hash password
|
// Hash password
|
||||||
let password_hash = hash_password(&payload.password)
|
let password_hash = hash_password(&payload.password)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ Password hashing failed: {}", e); // ← ADD
|
error!("❌ Password hashing failed: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Failed to hash password" }))
|
Json(json!({ "error": "Failed to hash password" }))
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
debug!("✅ Password hashed successfully"); // ← ADD
|
debug!("✅ Password hashed successfully");
|
||||||
|
|
||||||
// Build bank_user_id from environment + number
|
// Build bank_user_id from environment + number
|
||||||
let bank_user_id = format!("{}-{}", state.bank_team_id, payload.bank_user_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
|
// Create user in database
|
||||||
let user = db::users::create_user(&state.db_pool, &bank_user_id, &password_hash)
|
let user = db::users::create_user(&state.db_pool, &bank_user_id, &password_hash)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ Database error during user creation: {}", e); // ← ADD
|
error!("❌ Database error during user creation: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::CONFLICT,
|
StatusCode::CONFLICT,
|
||||||
Json(json!({ "error": format!("User already exists or database error: {}", e) }))
|
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
|
// Generate JWT token
|
||||||
let token = generate_token(user.id, &user.bank_user_id)
|
let token = generate_token(user.id, &user.bank_user_id)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ JWT generation failed: {}", e); // ← ADD
|
error!("❌ JWT generation failed: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Failed to generate token" }))
|
Json(json!({ "error": "Failed to generate token" }))
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
info!("✅ JWT token generated"); // ← ADD
|
info!("✅ JWT token generated");
|
||||||
info!("🎉 REGISTER SUCCESSFUL for user: {}", bank_user_id); // ← ADD
|
info!("🎉 REGISTER SUCCESSFUL for user: {}", bank_user_id);
|
||||||
|
|
||||||
Ok::<_, (StatusCode, Json<serde_json::Value>)>((
|
Ok::<_, (StatusCode, Json<serde_json::Value>)>((
|
||||||
StatusCode::CREATED,
|
StatusCode::CREATED,
|
||||||
|
|
@ -116,22 +116,22 @@ pub async fn login_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Json(payload): Json<LoginRequest>,
|
Json(payload): Json<LoginRequest>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
info!("🔑 LOGIN REQUEST"); // ← ADD
|
info!("🔑 LOGIN REQUEST");
|
||||||
debug!(" bank_user_id: {}", payload.bank_user_id); // ← ADD
|
debug!(" bank_user_id: {}", payload.bank_user_id);
|
||||||
debug!(" password length: {} chars", payload.password.len()); // ← ADD
|
debug!(" password length: {} chars", payload.password.len());
|
||||||
|
|
||||||
// Look up user by bank_user_id
|
// 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)
|
let user_data = db::users::get_user_by_bank_user_id(&state.db_pool, &payload.bank_user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ Database error: {}", e); // ← ADD
|
error!("❌ Database error: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Database error" }))
|
Json(json!({ "error": "Database error" }))
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
warn!("⚠️ User not found: {}", payload.bank_user_id); // ← ADD
|
warn!("⚠️ User not found: {}", payload.bank_user_id);
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(json!({ "error": "Invalid bank_user_id or password" }))
|
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;
|
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
|
// Verify password
|
||||||
let is_valid = verify_password(&payload.password, &password_hash)
|
let is_valid = verify_password(&payload.password, &password_hash)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ Password verification error: {}", e); // ← ADD
|
error!("❌ Password verification error: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Password verification failed" }))
|
Json(json!({ "error": "Password verification failed" }))
|
||||||
|
|
@ -152,27 +152,27 @@ pub async fn login_handler(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !is_valid {
|
if !is_valid {
|
||||||
warn!("⚠️ Invalid password for user: {}", payload.bank_user_id); // ← ADD
|
warn!("⚠️ Invalid password for user: {}", payload.bank_user_id);
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(json!({ "error": "Invalid bank_user_id or password" }))
|
Json(json!({ "error": "Invalid bank_user_id or password" }))
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("✅ Password verified"); // ← ADD
|
info!("✅ Password verified");
|
||||||
|
|
||||||
// Generate JWT token
|
// Generate JWT token
|
||||||
let token = generate_token(user_id, &payload.bank_user_id)
|
let token = generate_token(user_id, &payload.bank_user_id)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!("❌ JWT generation failed: {}", e); // ← ADD
|
error!("❌ JWT generation failed: {}", e);
|
||||||
(
|
(
|
||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Json(json!({ "error": "Failed to generate token" }))
|
Json(json!({ "error": "Failed to generate token" }))
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
info!("✅ JWT token generated"); // ← ADD
|
info!("✅ JWT token generated");
|
||||||
info!("🎉 LOGIN SUCCESSFUL for user: {}", payload.bank_user_id); // ← ADD
|
info!("🎉 LOGIN SUCCESSFUL for user: {}", payload.bank_user_id);
|
||||||
|
|
||||||
Ok::<_, (StatusCode, Json<serde_json::Value>)>((
|
Ok::<_, (StatusCode, Json<serde_json::Value>)>((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
@ -186,7 +186,7 @@ pub async fn login_handler(
|
||||||
pub async fn me_handler(
|
pub async fn me_handler(
|
||||||
axum::extract::Extension(claims): axum::extract::Extension<super::jwt::Claims>,
|
axum::extract::Extension(claims): axum::extract::Extension<super::jwt::Claims>,
|
||||||
) -> impl IntoResponse {
|
) -> 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,
|
StatusCode::OK,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
// src/auth/jwt.rs
|
// 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 jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -9,9 +7,9 @@ use std::env;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Claims {
|
pub struct Claims {
|
||||||
pub sub: i32, // Internal user_id from database
|
pub sub: i32,
|
||||||
pub bank_user_id: String, // e.g., "team275-6" - identifies which Multiberry user
|
pub bank_user_id: String,
|
||||||
pub exp: i64, // Expiration timestamp
|
pub exp: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_token(user_id: i32, bank_user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
pub fn generate_token(user_id: i32, bank_user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/auth/middleware.rs
|
// src/auth/middleware.rs
|
||||||
// Axum middleware to protect routes with JWT authentication
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
|
|
@ -9,6 +8,7 @@ use axum::{
|
||||||
response::Response,
|
response::Response,
|
||||||
Json,
|
Json,
|
||||||
};
|
};
|
||||||
|
use tracing::{info, error};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::jwt::validate_token;
|
use super::jwt::validate_token;
|
||||||
|
|
@ -31,6 +31,7 @@ pub async fn auth_middleware(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
Json(json!({ "error": "Invalid or expired token" }))
|
Json(json!({ "error": "Invalid or expired token" }))
|
||||||
))?;
|
))?;
|
||||||
|
info!("🔑auth_middleware token: {}", token);
|
||||||
|
|
||||||
req.extensions_mut().insert(claims);
|
req.extensions_mut().insert(claims);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/auth/password.rs
|
// src/auth/password.rs
|
||||||
// Password hashing and verification using Argon2
|
|
||||||
|
|
||||||
use argon2::{
|
use argon2::{
|
||||||
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use chrono::NaiveDate;
|
||||||
pub async fn store_account(
|
pub async fn store_account(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
account_id: &str,
|
account_id: &str,
|
||||||
user_id: &str, // team275-6
|
user_id: &str,
|
||||||
bank_code: &str, // vbank
|
bank_code: &str,
|
||||||
status: &str,
|
status: &str,
|
||||||
currency: &str,
|
currency: &str,
|
||||||
account_type: &str,
|
account_type: &str,
|
||||||
|
|
@ -46,7 +46,7 @@ pub async fn get_accounts_for_user(
|
||||||
pool: &PgPool,
|
pool: &PgPool,
|
||||||
user_id: &str,
|
user_id: &str,
|
||||||
bank_code: &str,
|
bank_code: &str,
|
||||||
) -> Result<Vec<(String, String)>, sqlx::Error> { // Returns (account_id, nickname)
|
) -> Result<Vec<(String, String)>, sqlx::Error> {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT account_id, nickname
|
SELECT account_id, nickname
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ mod api;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod db;
|
mod db;
|
||||||
mod route;
|
mod route;
|
||||||
mod state; // ← Already have this
|
mod state;
|
||||||
|
|
||||||
use state::AppState;
|
use state::AppState;
|
||||||
|
|
||||||
|
|
@ -16,7 +16,6 @@ use state::AppState;
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
// Initialize tracing with detailed logging
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_max_level(tracing::Level::DEBUG)
|
.with_max_level(tracing::Level::DEBUG)
|
||||||
.with_target(true)
|
.with_target(true)
|
||||||
|
|
@ -28,14 +27,13 @@ async fn main() {
|
||||||
|
|
||||||
let app_state = AppState::new().await;
|
let app_state = AppState::new().await;
|
||||||
|
|
||||||
// ✨ Add CORS layer
|
|
||||||
let cors = CorsLayer::new()
|
let cors = CorsLayer::new()
|
||||||
.allow_origin(Any)
|
.allow_origin(Any)
|
||||||
.allow_methods(Any)
|
.allow_methods(Any)
|
||||||
.allow_headers(Any);
|
.allow_headers(Any);
|
||||||
|
|
||||||
let app = route::router(app_state)
|
let app = route::router(app_state)
|
||||||
.layer(cors) // ← Add CORS before TraceLayer
|
.layer(cors)
|
||||||
.layer(
|
.layer(
|
||||||
TraceLayer::new_for_http()
|
TraceLayer::new_for_http()
|
||||||
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
|
.make_span_with(DefaultMakeSpan::new().level(Level::INFO))
|
||||||
|
|
|
||||||
15
src/route.rs
15
src/route.rs
|
|
@ -12,35 +12,34 @@ use crate::{auth, state::AppState};
|
||||||
pub mod handlers;
|
pub mod handlers;
|
||||||
|
|
||||||
pub fn router(app_state: AppState) -> Router {
|
pub fn router(app_state: AppState) -> Router {
|
||||||
// Public routes (no auth required)
|
|
||||||
let public_routes = Router::new()
|
let public_routes = Router::new()
|
||||||
.route("/api/health", get(handlers::health_handler))
|
.route("/api/health", get(handlers::health_handler))
|
||||||
.route("/api/auth/register", post(auth::handlers::register_handler))
|
.route("/api/auth/register", post(auth::handlers::register_handler))
|
||||||
.route("/api/auth/login", post(auth::handlers::login_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()
|
let protected_routes = Router::new()
|
||||||
.route("/api/auth/me", get(auth::handlers::me_handler))
|
.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)
|
post(handlers::create_consent_handler)
|
||||||
.get(handlers::get_consent_handler)
|
.get(handlers::get_consent_handler)
|
||||||
.delete(handlers::delete_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)
|
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)
|
get(handlers::get_transactions_handler)
|
||||||
)
|
)
|
||||||
.route("/api/transactions", // ← Get all transactions (user from JWT)
|
.route("/api/transactions",
|
||||||
get(handlers::get_all_transactions_handler)
|
get(handlers::get_all_transactions_handler)
|
||||||
)
|
)
|
||||||
.route("/api/balances/{bank}/{account_id}",
|
.route("/api/balances/{bank}/{account_id}",
|
||||||
get(handlers::get_balances_handler)
|
get(handlers::get_balances_handler)
|
||||||
)
|
)
|
||||||
.layer(middleware::from_fn(auth::auth_middleware)); // ← Your existing middleware
|
.layer(middleware::from_fn(auth::auth_middleware));
|
||||||
|
|
||||||
// Merge both
|
|
||||||
public_routes
|
public_routes
|
||||||
.merge(protected_routes)
|
.merge(protected_routes)
|
||||||
.with_state(app_state)
|
.with_state(app_state)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
// src/route/handlers.rs
|
// src/route/handlers.rs
|
||||||
// HTTP request handlers for the banking API
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, Query, State},
|
extract::{Path, Query, State},
|
||||||
|
|
@ -50,21 +49,22 @@ struct ConsentCreatedResponse {
|
||||||
|
|
||||||
pub async fn create_consent_handler(
|
pub async fn create_consent_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(bank_code): Path<String>, // ← Only bank_code now
|
Path(bank_code): Path<String>,
|
||||||
Extension(claims): Extension<Claims>, // ← Get user_id from JWT!
|
Extension(claims): Extension<Claims>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &claims.bank_user_id; // ← Extract from JWT
|
let user_id = &claims.bank_user_id;
|
||||||
|
|
||||||
let bank = bank_code.parse::<Bank>()
|
let bank = bank_code.parse::<Bank>()
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({
|
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({
|
||||||
"error": "Invalid bank code. Use: vbank, abank, or sbank"
|
"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 client = state.banking_clients.get_client(bank);
|
||||||
|
|
||||||
let consent_response = client
|
let consent_response = client
|
||||||
.request_consent(user_id) // ← Use extracted user_id
|
.request_consent(user_id)
|
||||||
.await
|
.await
|
||||||
.map_err(map_banking_error)?;
|
.map_err(map_banking_error)?;
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ pub async fn create_consent_handler(
|
||||||
|
|
||||||
db::consents::store_consent(
|
db::consents::store_consent(
|
||||||
&state.db_pool,
|
&state.db_pool,
|
||||||
user_id, // ← Use extracted user_id
|
user_id,
|
||||||
bank.code(),
|
bank.code(),
|
||||||
&consent_response.consent_id,
|
&consent_response.consent_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
|
|
@ -100,16 +100,18 @@ pub async fn create_consent_handler(
|
||||||
|
|
||||||
pub async fn get_consent_handler(
|
pub async fn get_consent_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(bank_code): Path<String>, // ← Only bank_code now
|
Path(bank_code): Path<String>,
|
||||||
Extension(claims): Extension<Claims>, // ← Get user_id from JWT!
|
Extension(claims): Extension<Claims>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &claims.bank_user_id; // ← Extract from JWT
|
let user_id = &claims.bank_user_id;
|
||||||
|
|
||||||
let bank = bank_code.parse::<Bank>()
|
let bank = bank_code.parse::<Bank>()
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({
|
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({
|
||||||
"error": "Invalid bank code"
|
"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())
|
db::consents::get_valid_consent(&state.db_pool, user_id, bank.code())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (
|
.map_err(|e| (
|
||||||
|
|
@ -130,10 +132,10 @@ pub async fn get_consent_handler(
|
||||||
|
|
||||||
pub async fn get_accounts_handler(
|
pub async fn get_accounts_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(bank_code): Path<String>, // ← Only bank_code now
|
Path(bank_code): Path<String>,
|
||||||
Extension(claims): Extension<Claims>, // ← Get user_id from JWT!
|
Extension(claims): Extension<Claims>,
|
||||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let user_id = &claims.bank_user_id; // ← Extract from JWT
|
let user_id = &claims.bank_user_id;
|
||||||
|
|
||||||
let bank = bank_code.parse::<Bank>()
|
let bank = bank_code.parse::<Bank>()
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?;
|
.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." }))
|
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 client = state.banking_clients.get_client(bank);
|
||||||
|
|
||||||
let accounts_response = client.get_accounts(user_id, &consent_id)
|
let accounts_response = client.get_accounts(user_id, &consent_id)
|
||||||
.await
|
.await
|
||||||
.map_err(map_banking_error)?;
|
.map_err(map_banking_error)?;
|
||||||
|
|
||||||
// ✨ Save accounts to database
|
// Save accounts to database
|
||||||
for account in &accounts_response.data.account {
|
for account in &accounts_response.data.account {
|
||||||
let _ = db::accounts::store_account(
|
let _ = db::accounts::store_account(
|
||||||
&state.db_pool,
|
&state.db_pool,
|
||||||
&account.account_id,
|
&account.account_id,
|
||||||
user_id, // ← Use extracted user_id
|
user_id,
|
||||||
bank.code(),
|
bank.code(),
|
||||||
account.status.as_deref().unwrap_or("unknown"),
|
account.status.as_deref().unwrap_or("unknown"),
|
||||||
&account.currency,
|
&account.currency,
|
||||||
|
|
@ -183,15 +187,17 @@ pub struct TransactionQuery {
|
||||||
|
|
||||||
pub async fn get_transactions_handler(
|
pub async fn get_transactions_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((bank_code, account_id)): Path<(String, String)>, // ← Changed: no user_id
|
Path((bank_code, account_id)): Path<(String, String)>,
|
||||||
Query(params): Query<TransactionQuery>,
|
Query(params): Query<TransactionQuery>,
|
||||||
Extension(claims): Extension<Claims>, // ← Get user_id from JWT!
|
Extension(claims): Extension<Claims>,
|
||||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let user_id = &claims.bank_user_id; // ← Extract from JWT
|
let user_id = &claims.bank_user_id;
|
||||||
|
|
||||||
|
|
||||||
let bank = bank_code.parse::<Bank>()
|
let bank = bank_code.parse::<Bank>()
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?;
|
.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())
|
let consent_id = db::consents::get_valid_consent(&state.db_pool, user_id, bank.code())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?
|
||||||
|
|
@ -206,7 +212,7 @@ pub async fn get_transactions_handler(
|
||||||
.await
|
.await
|
||||||
.map_err(map_banking_error)?;
|
.map_err(map_banking_error)?;
|
||||||
|
|
||||||
// ✨ Save all transactions to cache
|
// Save all transactions to cache
|
||||||
for tx in &transactions_response.data.transaction {
|
for tx in &transactions_response.data.transaction {
|
||||||
let _ = db::transactions::store_transaction(
|
let _ = db::transactions::store_transaction(
|
||||||
&state.db_pool,
|
&state.db_pool,
|
||||||
|
|
@ -230,15 +236,14 @@ pub async fn get_transactions_handler(
|
||||||
pub async fn get_all_transactions_handler(
|
pub async fn get_all_transactions_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Query(params): Query<TransactionQuery>,
|
Query(params): Query<TransactionQuery>,
|
||||||
Extension(claims): Extension<Claims>, // ← Add this
|
Extension(claims): Extension<Claims>,
|
||||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let bank_user_id = &claims.bank_user_id;
|
let bank_user_id = &claims.bank_user_id;
|
||||||
let page = params.page.unwrap_or(1);
|
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 {
|
if limit > 100 {
|
||||||
return Err((
|
return Err((
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
|
|
@ -248,7 +253,6 @@ pub async fn get_all_transactions_handler(
|
||||||
|
|
||||||
let offset = (page - 1) * limit;
|
let offset = (page - 1) * limit;
|
||||||
|
|
||||||
// Get total count
|
|
||||||
let total_count: i64 = sqlx::query_scalar(
|
let total_count: i64 = sqlx::query_scalar(
|
||||||
"SELECT COUNT(*) FROM transactions WHERE account_id IN (
|
"SELECT COUNT(*) FROM transactions WHERE account_id IN (
|
||||||
SELECT account_id FROM accounts WHERE user_id = $1
|
SELECT account_id FROM accounts WHERE user_id = $1
|
||||||
|
|
@ -259,7 +263,6 @@ pub async fn get_all_transactions_handler(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?;
|
||||||
|
|
||||||
// Get paginated transactions
|
|
||||||
let transactions = sqlx::query!(
|
let transactions = sqlx::query!(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -316,14 +319,16 @@ pub async fn get_all_transactions_handler(
|
||||||
|
|
||||||
pub async fn delete_consent_handler(
|
pub async fn delete_consent_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(bank_code): Path<String>, // ← Only bank_code now
|
Path(bank_code): Path<String>,
|
||||||
Extension(claims): Extension<Claims>, // ← Get user_id from JWT!
|
Extension(claims): Extension<Claims>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let user_id = &claims.bank_user_id; // ← Extract from JWT
|
let user_id = &claims.bank_user_id;
|
||||||
|
|
||||||
let bank = bank_code.parse::<Bank>()
|
let bank = bank_code.parse::<Bank>()
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?;
|
.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())
|
let consent_id = db::consents::get_valid_consent(&state.db_pool, user_id, bank.code())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?
|
||||||
|
|
@ -331,12 +336,10 @@ pub async fn delete_consent_handler(
|
||||||
|
|
||||||
let client = state.banking_clients.get_client(bank);
|
let client = state.banking_clients.get_client(bank);
|
||||||
|
|
||||||
// Delete from bank first
|
|
||||||
client.delete_consent(&consent_id)
|
client.delete_consent(&consent_id)
|
||||||
.await
|
.await
|
||||||
.map_err(map_banking_error)?;
|
.map_err(map_banking_error)?;
|
||||||
|
|
||||||
// Then delete from our DB
|
|
||||||
db::consents::delete_consent(&state.db_pool, &consent_id)
|
db::consents::delete_consent(&state.db_pool, &consent_id)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?;
|
.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(
|
pub async fn get_balances_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path((bank_code, account_id)): Path<(String, String)>, // ← account_id, not user_id!
|
Path((bank_code, account_id)): Path<(String, String)>,
|
||||||
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, Json<serde_json::Value>)> {
|
||||||
let bank = bank_code.parse::<Bank>()
|
let bank = bank_code.parse::<Bank>()
|
||||||
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?;
|
.map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid bank code" }))))?;
|
||||||
|
|
||||||
// We need to get consent from the account's user
|
info!("💰 get_balances account_id: {} bank: {}", account_id, bank.code());
|
||||||
// For now, just get the first valid consent for this bank
|
|
||||||
let consent_id = db::consents::get_first_valid_consent(&state.db_pool, bank.code())
|
let consent_id = db::consents::get_first_valid_consent(&state.db_pool, bank.code())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))?
|
.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);
|
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)?;
|
let response = client.get_balances(&account_id, &consent_id).await.map_err(map_banking_error)?;
|
||||||
|
|
||||||
// ✨ Cache balances
|
|
||||||
for balance in &response.data.balance {
|
for balance in &response.data.balance {
|
||||||
let _ = db::balances::store_balance(
|
let _ = db::balances::store_balance(
|
||||||
&state.db_pool,
|
&state.db_pool,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ use crate::api::BankingClients;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
/// Global application state, accessible in all handlers
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub db_pool: PgPool,
|
pub db_pool: PgPool,
|
||||||
pub banking_clients: BankingClients,
|
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 {
|
impl AppState {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue