diff --git a/src/route.rs b/src/route.rs index c49f945..57f43c5 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,10 +1,16 @@ // src/route.rs +// CORRECT VERSION - keeps your protected_routes pattern + +use axum::{ + middleware, + routing::{get, post}, + Router, +}; + +use crate::{auth, state::AppState}; pub mod handlers; -use axum::{middleware, routing::{get, post}, Router}; -use crate::{state::AppState, auth}; - pub fn router(app_state: AppState) -> Router { // Public routes (no auth required) let public_routes = Router::new() @@ -12,31 +18,30 @@ pub fn router(app_state: AppState) -> Router { .route("/api/auth/register", post(auth::handlers::register_handler)) .route("/api/auth/login", post(auth::handlers::login_handler)); - // Protected routes (auth required) + // 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}/{user_id}", + .route("/api/consent/{bank}", // ← Removed {user_id} - get from JWT! post(handlers::create_consent_handler) - .get(handlers::get_consent_handler) - .delete(handlers::delete_consent_handler) + .get(handlers::get_consent_handler) + .delete(handlers::delete_consent_handler) ) - .route("/api/accounts/{bank}/{user_id}", + .route("/api/accounts/{bank}", // ← Removed {user_id} - get from JWT! get(handlers::get_accounts_handler) ) - .route("/api/transactions/{bank}/{user_id}/{account_id}", + .route("/api/transactions/{bank}/{account_id}", // ← Removed {user_id} get(handlers::get_transactions_handler) ) - .route("/api/transactions/{bank_user_id}", - get(handlers::get_all_transactions_handler) // ← Новый эндпоинт + .route("/api/transactions", // ← Get all transactions (user from JWT) + get(handlers::get_all_transactions_handler) ) .route("/api/balances/{bank}/{account_id}", get(handlers::get_balances_handler) ) - .layer(middleware::from_fn(auth::auth_middleware)); + .layer(middleware::from_fn(auth::auth_middleware)); // ← Your existing middleware // Merge both public_routes .merge(protected_routes) .with_state(app_state) } - diff --git a/src/route/handlers.rs b/src/route/handlers.rs index 741efda..c7c3dea 100644 --- a/src/route/handlers.rs +++ b/src/route/handlers.rs @@ -15,7 +15,9 @@ use crate::{ db, }; +use crate::auth::jwt::Claims; use tracing::info; +use axum::Extension; // --- Health Check --- @@ -48,29 +50,33 @@ struct ConsentCreatedResponse { pub async fn create_consent_handler( State(state): State, - Path((bank_code, user_id)): Path<(String, String)>, + Path(bank_code): Path, // ← Only bank_code now + Extension(claims): Extension, // ← Get user_id from JWT! ) -> impl IntoResponse { + let user_id = &claims.bank_user_id; // ← Extract from JWT + 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 let client = state.banking_clients.get_client(bank); let consent_response = client - .request_consent(&user_id) + .request_consent(user_id) // ← Use extracted user_id .await .map_err(map_banking_error)?; let expires_at = chrono::DateTime::parse_from_rfc3339(&consent_response.created_at) .ok() .map(|dt| dt.with_timezone(&chrono::Utc)) - .unwrap_or_else(|| chrono::Utc::now()) // fallback to now if parse fails + .unwrap_or_else(|| chrono::Utc::now()) + chrono::Duration::days(365); db::consents::store_consent( &state.db_pool, - &user_id, + user_id, // ← Use extracted user_id bank.code(), &consent_response.consent_id, expires_at, @@ -94,14 +100,17 @@ pub async fn create_consent_handler( pub async fn get_consent_handler( State(state): State, - Path((bank_code, user_id)): Path<(String, String)>, + Path(bank_code): Path, // ← Only bank_code now + Extension(claims): Extension, // ← Get user_id from JWT! ) -> impl IntoResponse { + let user_id = &claims.bank_user_id; // ← Extract from JWT + let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid 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 .map_err(|e| ( StatusCode::INTERNAL_SERVER_ERROR, @@ -121,12 +130,15 @@ pub async fn get_consent_handler( pub async fn get_accounts_handler( State(state): State, - Path((bank_code, user_id)): Path<(String, String)>, + Path(bank_code): Path, // ← Only bank_code now + Extension(claims): Extension, // ← Get user_id from JWT! ) -> Result, (StatusCode, Json)> { + let user_id = &claims.bank_user_id; // ← Extract from JWT + let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid 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 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))? .ok_or_else(|| ( @@ -136,16 +148,16 @@ pub async fn get_accounts_handler( 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 .map_err(map_banking_error)?; - // ✨ NEW: 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, + user_id, // ← Use extracted user_id bank.code(), account.status.as_deref().unwrap_or("unknown"), &account.currency, @@ -171,13 +183,16 @@ pub struct TransactionQuery { pub async fn get_transactions_handler( State(state): State, - Path((bank_code, user_id, account_id)): Path<(String, String, String)>, + Path((bank_code, account_id)): Path<(String, String)>, // ← Changed: no user_id Query(params): Query, + Extension(claims): Extension, // ← Get user_id from JWT! ) -> Result, (StatusCode, Json)> { + let user_id = &claims.bank_user_id; // ← Extract from JWT + let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid 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 .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({ "error": e.to_string() }))))? .ok_or_else(|| ( @@ -191,7 +206,7 @@ pub async fn get_transactions_handler( .await .map_err(map_banking_error)?; - // ✨ NEW: 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, @@ -202,8 +217,8 @@ pub async fn get_transactions_handler( &tx.amount.currency, &tx.credit_debit_indicator, &tx.status, - tx.booking_date_time, // Already DateTime - tx.value_date_time, // Already Option> + tx.booking_date_time, + tx.value_date_time, &tx.transaction_information, tx.bank_transaction_code.as_ref().map(|b| b.code.as_str()), ).await; @@ -214,9 +229,10 @@ pub async fn get_transactions_handler( pub async fn get_all_transactions_handler( State(state): State, - Path(bank_user_id): Path, - Query(params): Query, // ← Добавили query params + Query(params): Query, + Extension(claims): Extension, // ← Add this ) -> 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 на странице для ленты @@ -300,12 +316,15 @@ pub async fn get_all_transactions_handler( pub async fn delete_consent_handler( State(state): State, - Path((bank_code, user_id)): Path<(String, String)>, + Path(bank_code): Path, // ← Only bank_code now + Extension(claims): Extension, // ← Get user_id from JWT! ) -> impl IntoResponse { + let user_id = &claims.bank_user_id; // ← Extract from JWT + let bank = bank_code.parse::() .map_err(|_| (StatusCode::BAD_REQUEST, Json(json!({ "error": "Invalid 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 .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" }))))?; diff --git a/test_multiberry.sh b/test_multiberry.sh index cec5468..ab9cacb 100644 --- a/test_multiberry.sh +++ b/test_multiberry.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Save as test_multiberry.sh -# Extended comprehensive test with DB verification +# Save as test_multiberry_secure.sh +# Tests secure API - user_id extracted from JWT token automatically BASE_URL="http://localhost:3000/api" @@ -9,7 +9,7 @@ echo "1️⃣ REGISTER USER" echo "==========================================" REGISTER=$(curl -s -X POST $BASE_URL/auth/register \ -H 'Content-Type: application/json' \ - -d '{"bank_user_number": 1, "password": "testpass123"}') + -d '{"bank_user_number": 2, "password": "testpass123"}') echo "$REGISTER" | jq . TOKEN=$(echo "$REGISTER" | jq -r '.token') @@ -30,14 +30,14 @@ echo "" echo "==========================================" echo "3️⃣ GET ME (verify auth middleware)" echo "==========================================" -curl -s http://localhost:3000/api/auth/me \ +curl -s $BASE_URL/auth/me \ -H "Authorization: Bearer $TOKEN" | jq . echo "" echo "==========================================" -echo "4️⃣ REQUEST CONSENT from VBank" +echo "4️⃣ REQUEST CONSENT from VBank (user from JWT)" echo "==========================================" -CONSENT=$(curl -s -X POST $BASE_URL/consent/vbank/$BANK_USER_ID \ +CONSENT=$(curl -s -X POST $BASE_URL/consent/vbank \ -H "Authorization: Bearer $TOKEN") echo "$CONSENT" | jq . @@ -46,9 +46,9 @@ echo "✅ Consent ID: $CONSENT_ID" echo "" echo "==========================================" -echo "5️⃣ GET ACCOUNTS (auto-saved to DB)" +echo "5️⃣ GET ACCOUNTS (user from JWT, auto-saved to DB)" echo "==========================================" -ACCOUNTS=$(curl -s $BASE_URL/accounts/vbank/$BANK_USER_ID \ +ACCOUNTS=$(curl -s $BASE_URL/accounts/vbank \ -H "Authorization: Bearer $TOKEN") echo "$ACCOUNTS" | jq . @@ -68,9 +68,9 @@ echo "✅ Current Balance: $BALANCE_AMOUNT RUB" echo "" echo "==========================================" -echo "7️⃣ GET TRANSACTIONS (page 1, limit 6 - auto-saved)" +echo "7️⃣ GET TRANSACTIONS (user from JWT, page 1, limit 6)" echo "==========================================" -TRANS_PAGE1=$(curl -s "$BASE_URL/transactions/vbank/$BANK_USER_ID/$ACCOUNT_ID?page=1&limit=6" \ +TRANS_PAGE1=$(curl -s "$BASE_URL/transactions/vbank/$ACCOUNT_ID?page=1&limit=6" \ -H "Authorization: Bearer $TOKEN") echo "$TRANS_PAGE1" | jq . @@ -81,54 +81,38 @@ echo "" echo "==========================================" echo "8️⃣ GET TRANSACTIONS (page 2, limit 6)" echo "==========================================" -curl -s "$BASE_URL/transactions/vbank/$BANK_USER_ID/$ACCOUNT_ID?page=2&limit=6" \ +curl -s "$BASE_URL/transactions/vbank/$ACCOUNT_ID?page=2&limit=6" \ -H "Authorization: Bearer $TOKEN" | jq . echo "" echo "==========================================" -echo "9️⃣ DELETE CONSENT" +echo "9️⃣ GET ALL TRANSACTIONS (unified view from all banks)" echo "==========================================" -curl -s -X DELETE $BASE_URL/consent/vbank/$BANK_USER_ID \ +curl -s "$BASE_URL/transactions?page=1&limit=10" \ -H "Authorization: Bearer $TOKEN" | jq . echo "" echo "==========================================" -echo "🔟 VERIFY DATABASE CACHE" +echo "🔟 DELETE CONSENT (user from JWT)" echo "==========================================" -echo "" -echo "Run in another terminal:" -echo "just psql-exec" -echo "" -echo "Then execute these queries:" -echo "" -echo "-- Check users registered:" -echo "SELECT bank_user_id, created_at FROM users ORDER BY created_at DESC LIMIT 5;" -echo "" -echo "-- Check consents granted:" -echo "SELECT user_id, bank_code, consent_id, status, expires_at FROM user_consents;" -echo "" -echo "-- Check accounts cached:" -echo "SELECT account_id, user_id, bank_code, nickname, currency FROM accounts;" -echo "" -echo "-- Check balances cached:" -echo "SELECT account_id, balance_type, amount, currency, date_time FROM balances;" -echo "" -echo "-- Check transactions cached (show count by account):" -echo "SELECT account_id, COUNT(*) as tx_count, MIN(booking_date_time) as oldest, MAX(booking_date_time) as newest FROM transactions GROUP BY account_id;" -echo "" -echo "-- Show recent transactions:" -echo "SELECT transaction_id, account_id, amount, currency, credit_debit_indicator, transaction_information, booking_date_time FROM transactions ORDER BY booking_date_time DESC LIMIT 10;" +curl -s -X DELETE $BASE_URL/consent/vbank \ + -H "Authorization: Bearer $TOKEN" | jq . echo "" echo "==========================================" -echo "✅ FULL TEST COMPLETE!" +echo "✅ FULL SECURE TEST COMPLETE!" echo "==========================================" echo "" -echo "Summary:" -echo "✅ Authentication (register/login/auth middleware)" -echo "✅ Consent Management (request consent)" -echo "✅ Account Aggregation (fetch & cache accounts)" -echo "✅ Balance Retrieval (fetch & cache balances)" -echo "✅ Transaction History (fetch & cache transactions)" -echo "✅ Data Persistence (all cached in PostgreSQL)" +echo "🔒 Security Benefits:" +echo " ✅ User cannot manipulate user_id in URL" +echo " ✅ All user identification comes from JWT" +echo " ✅ Frontend only needs to send token" +echo " ✅ Backend automatically knows WHO is making request" echo "" +echo "📊 Data Aggregation:" +echo " ✅ Accounts cached from bank" +echo " ✅ Balances cached" +echo " ✅ Transactions cached" +echo " ✅ Multi-bank support ready" +echo "" +