diff --git a/.sqlx/query-2aeca288c85e977c8515a4231fddc9944bac2071c8ffa37973f1d4bd723f224e.json b/.sqlx/query-2aeca288c85e977c8515a4231fddc9944bac2071c8ffa37973f1d4bd723f224e.json new file mode 100644 index 0000000..bc300fb --- /dev/null +++ b/.sqlx/query-2aeca288c85e977c8515a4231fddc9944bac2071c8ffa37973f1d4bd723f224e.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM user_consents WHERE consent_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [] + }, + "hash": "2aeca288c85e977c8515a4231fddc9944bac2071c8ffa37973f1d4bd723f224e" +} diff --git a/justfile b/justfile index 288ee25..8a56a05 100644 --- a/justfile +++ b/justfile @@ -36,6 +36,9 @@ db-reset: @docker compose down -v @{{sops_run}} 'docker compose up -d' +psql-exec: + @{{sops_run}} 'psql $DATABASE_URL' + stop: @echo "🛑 Stopping all services..." @docker compose down diff --git a/src/api/accounts.rs b/src/api/accounts.rs index 259c5c7..2d7bdbd 100644 --- a/src/api/accounts.rs +++ b/src/api/accounts.rs @@ -1,7 +1,7 @@ // src/api/accounts.rs // Account data retrieval -use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData}}; +use super::{client::{BankClient, BankingError}, models::{ApiResponse, AccountData, TransactionData}}; impl BankClient { pub async fn get_accounts( @@ -28,4 +28,5 @@ impl BankClient { }), } } + } diff --git a/src/api/consents.rs b/src/api/consents.rs index 1d692f8..37ec845 100644 --- a/src/api/consents.rs +++ b/src/api/consents.rs @@ -62,4 +62,32 @@ impl BankClient { consent }) } + + pub async fn delete_consent(&self, consent_id: &str) -> Result<(), BankingError> { + info!("🗑️ Deleting consent: {}", consent_id); + + let token = self.get_token().await?; + + let response = self.http_client + .delete(self.base_url.join(&format!("/account-consents/{}", consent_id))?) + .bearer_auth(token) + .header("x-fapi-interaction-id", format!("team275-{}", chrono::Utc::now().timestamp())) + .send() + .await?; + + match response.status().as_u16() { + 204 => { + info!("✅ Consent deleted successfully"); + Ok(()) + }, + status => { + error!("❌ Failed to delete consent: {}", status); + Err(BankingError::ApiError { + status, + body: response.text().await.unwrap_or_default(), + }) + } + } + } + } diff --git a/src/db/consents.rs b/src/db/consents.rs index 459ec65..7a8e2cd 100644 --- a/src/db/consents.rs +++ b/src/db/consents.rs @@ -3,6 +3,7 @@ use sqlx::PgPool; use chrono::{DateTime, Utc}; +use tracing::{info}; pub struct StoredConsent { pub user_id: String, @@ -55,3 +56,20 @@ pub async fn get_valid_consent( Ok(result.map(|r| r.consent_id)) } + +pub async fn delete_consent( + pool: &PgPool, + consent_id: &str, +) -> Result<(), sqlx::Error> { + info!("🗑️ Deleting consent from DB: {}", consent_id); + + sqlx::query!( + "DELETE FROM user_consents WHERE consent_id = $1", + consent_id + ) + .execute(pool) + .await?; + + info!("✅ Consent deleted from DB"); + Ok(()) +} diff --git a/src/route.rs b/src/route.rs index 9335863..d1d2f88 100644 --- a/src/route.rs +++ b/src/route.rs @@ -18,6 +18,7 @@ pub fn router(app_state: AppState) -> Router { .route("/api/consent/{bank}/{user_id}", post(handlers::create_consent_handler) .get(handlers::get_consent_handler) + .delete(handlers::delete_consent_handler) ) .route("/api/accounts/{bank}/{user_id}", get(handlers::get_accounts_handler) diff --git a/src/route/handlers.rs b/src/route/handlers.rs index e799e9c..bef9db3 100644 --- a/src/route/handlers.rs +++ b/src/route/handlers.rs @@ -83,7 +83,7 @@ pub async fn create_consent_handler( StatusCode::CREATED, Json(json!({ "consent_id": consent_response.consent_id, - "expires_at": consent_response.created_at + &chrono::Duration::days(365).to_string(), + "expires_at": expires_at, "status": consent_response.status, "message": consent_response.message, })) @@ -134,12 +134,31 @@ pub async fn get_accounts_handler( let client = state.banking_clients.get_client(bank); - client.get_accounts(&user_id, &consent_id) + let accounts_response = client.get_accounts(&user_id, &consent_id) .await - .map(|accounts| Json(serde_json::to_value(accounts).unwrap())) - .map_err(map_banking_error) + .map_err(map_banking_error)?; + + // ✨ NEW: Save accounts to database + for account in &accounts_response.data.account { + let _ = db::accounts::store_account( + &state.db_pool, + &account.account_id, + &user_id, + bank.code(), + account.status.as_deref().unwrap_or("unknown"), + &account.currency, + &account.account_type, + account.account_sub_type.as_deref(), + &account.nickname, + account.description.as_deref(), + account.opening_date, + ).await; + } + + Ok(Json(serde_json::to_value(accounts_response).unwrap())) } + // --- Transaction Management --- #[derive(Debug, Deserialize)] @@ -172,6 +191,35 @@ pub async fn get_transactions_handler( .map_err(map_banking_error) } +pub async fn delete_consent_handler( + State(state): State, + Path((bank_code, user_id)): Path<(String, String)>, +) -> impl IntoResponse { + 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()) + .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() }))))?; + + Ok::<_, (StatusCode, Json)>((StatusCode::OK, Json(json!({ "status": "deleted" })))) +} + + + // --- Error Mapping --- fn map_banking_error(err: BankingError) -> (StatusCode, Json) { diff --git a/test_multiberry.sh b/test_multiberry.sh new file mode 100644 index 0000000..b675f9c --- /dev/null +++ b/test_multiberry.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Save as test_multiberry.sh + +BASE_URL="http://localhost:3000/api" + +echo "==========================================" +echo "1️⃣ REGISTER USER" +echo "==========================================" +REGISTER=$(curl -s -X POST $BASE_URL/auth/register \ + -H 'Content-Type: application/json' \ + -d '{"bank_user_number": 8, "password": "testpass123"}') + +echo "$REGISTER" | jq . +TOKEN=$(echo "$REGISTER" | jq -r '.token') +BANK_USER_ID=$(echo "$REGISTER" | jq -r '.bank_user_id') + +echo "✅ Token: ${TOKEN:0:50}..." +echo "✅ Bank User ID: $BANK_USER_ID" +echo "" + +echo "==========================================" +echo "2️⃣ LOGIN (verify token works)" +echo "==========================================" +LOGIN=$(curl -s -X POST $BASE_URL/auth/login \ + -H 'Content-Type: application/json' \ + -d "{\"bank_user_id\": \"$BANK_USER_ID\", \"password\": \"testpass123\"}") + +echo "$LOGIN" | jq . +echo "" + +echo "==========================================" +echo "3️⃣ GET ME (verify auth middleware)" +echo "==========================================" +curl -s http://localhost:3000/api/auth/me \ + -H "Authorization: Bearer $TOKEN" | jq . +echo "" + +echo "==========================================" +echo "4️⃣ REQUEST CONSENT from VBank" +echo "==========================================" +CONSENT=$(curl -s -X POST $BASE_URL/consent/vbank/$BANK_USER_ID \ + -H "Authorization: Bearer $TOKEN") + +echo "$CONSENT" | jq . +CONSENT_ID=$(echo "$CONSENT" | jq -r '.consent_id') +echo "✅ Consent ID: $CONSENT_ID" +echo "" + +echo "==========================================" +echo "5️⃣ GET ACCOUNTS (auto-saved to DB)" +echo "==========================================" +ACCOUNTS=$(curl -s $BASE_URL/accounts/vbank/$BANK_USER_ID \ + -H "Authorization: Bearer $TOKEN") + +echo "$ACCOUNTS" | jq . +ACCOUNT_ID=$(echo "$ACCOUNTS" | jq -r '.data.account[0].accountId') +echo "✅ Account ID: $ACCOUNT_ID" +echo "" + +echo "==========================================" +echo "6️⃣ GET BALANCES" +echo "==========================================" +curl -s $BASE_URL/balances/vbank/$BANK_USER_ID \ + -H "Authorization: Bearer $TOKEN" | jq . +echo "" + +echo "==========================================" +echo "7️⃣ GET TRANSACTIONS (page 1, limit 6)" +echo "==========================================" +curl -s "$BASE_URL/transactions/vbank/$BANK_USER_ID/$ACCOUNT_ID?page=1&limit=6" \ + -H "Authorization: Bearer $TOKEN" | jq . +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" \ + -H "Authorization: Bearer $TOKEN" | jq . +echo "" + +echo "==========================================" +echo "9️⃣ DELETE CONSENT" +echo "==========================================" +curl -s -X DELETE $BASE_URL/consent/vbank/$BANK_USER_ID \ + -H "Authorization: Bearer $TOKEN" | jq . +echo "" + +echo "==========================================" +echo "🔟 VERIFY DB (from another terminal)" +echo "==========================================" +echo "just psql-exec" +echo "SELECT * FROM users;" +echo "SELECT * FROM user_consents;" +echo "SELECT * FROM accounts;" +echo "SELECT * FROM transactions;" +