api.rs (7808B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 use std::sync::Arc; 21 22 use axum::{ 23 Json, Router, 24 body::Bytes, 25 extract::{Multipart, State}, 26 response::Redirect, 27 routing::get, 28 }; 29 use compact_str::{CompactString, ToCompactString}; 30 use libeufin_ebics::{ 31 cli::EbicsLogs, 32 ebics::{EbicsClient, EbicsErrKind, administrative::OrderInfo}, 33 keys::{BankKeys, ClientKeys}, 34 }; 35 use reqwest::StatusCode; 36 use serde::{Deserialize, Serialize}; 37 use sqlx::PgPool; 38 use taler_api::{ 39 api::RouterUtils, 40 auth::AuthMethod, 41 error::{ApiResult, bad_request, failure, failure_status}, 42 }; 43 use taler_build::long_version; 44 use taler_common::{api::LibtoolVersion, error_code::ErrorCode}; 45 use taler_macros::api_config; 46 use tower_http::services::ServeDir; 47 48 use crate::config::EbisyncHostCfg; 49 50 pub struct EbisyncState { 51 pub db: PgPool, 52 pub cfg: EbisyncHostCfg, 53 pub client: ClientKeys, 54 pub bank: BankKeys, 55 } 56 57 impl EbisyncState { 58 pub fn client<'a>(&'a self) -> anyhow::Result<EbicsClient<'a>> { 59 EbicsClient::new(self.cfg.ebics(), EbicsLogs { dir: None }) 60 } 61 62 pub async fn orders(&self) -> anyhow::Result<Vec<OrderInfo>> { 63 let mut hkd = self 64 .client()? 65 .hkd(&self.db, &self.client, &self.bank, false) 66 .await?; 67 hkd.partner.orders.retain(|it| it.order.is_upload()); 68 Ok(hkd.partner.orders) 69 } 70 } 71 72 #[api_config("taler-ebisync")] 73 #[derive(Debug, Clone, PartialEq, Eq, Serialize)] 74 pub struct Config<'a> { 75 spa_version: &'a str, 76 } 77 78 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 79 pub struct ListSubmitOrders { 80 pub orders: Vec<SubmitOrder>, 81 } 82 83 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 84 pub struct SubmitOrder { 85 pub id: String, 86 pub description: String, 87 } 88 89 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 90 pub struct SyncSubmit { 91 pub order: CompactString, 92 } 93 94 pub fn sync_api(state: Arc<EbisyncState>, spa: &str, auth: AuthMethod) -> Router { 95 Router::new() 96 .nest_service("/webui/", ServeDir::new(spa)) 97 .route("/", get(async || Redirect::permanent("/webui/"))) 98 .route( 99 "/submit", 100 get(async |State(state): State<Arc<EbisyncState>>| { 101 ApiResult::Ok(Json(ListSubmitOrders { 102 orders: state 103 .orders() 104 .await 105 .map_err(|e| { 106 failure_status( 107 ErrorCode::END, 108 e.to_compact_string(), 109 StatusCode::BAD_GATEWAY, 110 ) 111 })? 112 .into_iter() 113 .map(|it| SubmitOrder { 114 id: it.order.to_string(), 115 description: it.description, 116 }) 117 .collect(), 118 })) 119 }) 120 .post( 121 async |State(state): State<Arc<EbisyncState>>, mut multipart: Multipart| { 122 let mut order_id: Option<String> = None; 123 let mut xml: Option<Bytes> = None; 124 125 while let Some(field) = multipart 126 .next_field() 127 .await 128 .map_err(|_| bad_request("Invalid multipart"))? 129 { 130 let name = field.name().unwrap_or("").to_string(); 131 132 match name.as_str() { 133 "order" => { 134 order_id = Some( 135 field 136 .text() 137 .await 138 .map_err(|_| bad_request("Invalid order field"))?, 139 ); 140 } 141 "file" => { 142 // treat as file 143 let data = field 144 .bytes() 145 .await 146 .map_err(|_| bad_request("Invalid file"))?; 147 148 xml = Some(data); 149 } 150 _ => {} 151 } 152 } 153 154 let order_id = order_id.ok_or_else(|| { 155 failure(ErrorCode::GENERIC_PARAMETER_MISSING, "Missing order ID") 156 .with_path("order") 157 })?; 158 159 let xml = xml.ok_or_else(|| { 160 failure(ErrorCode::GENERIC_PARAMETER_MISSING, "Missing file") 161 })?; 162 163 // --- match lookup --- 164 let match_order = state 165 .orders() 166 .await 167 .map_err(|e| { 168 failure_status( 169 ErrorCode::END, 170 e.to_compact_string(), 171 StatusCode::BAD_GATEWAY, 172 ) 173 })? 174 .into_iter() 175 .find(|o| o.order.to_string() == order_id.as_str()) 176 .ok_or_else(|| { 177 failure_status( 178 ErrorCode::END, 179 format!("Unknown order '{}'", order_id), 180 StatusCode::NOT_FOUND, 181 ) 182 })?; 183 184 // --- upload with error mapping --- 185 let order = state 186 .client() 187 .map_err(|e| { 188 failure_status( 189 ErrorCode::END, 190 e.to_compact_string(), 191 StatusCode::BAD_GATEWAY, 192 ) 193 })? 194 .upload(&state.client, &state.bank, &match_order.order, xml.as_ref()) 195 .await 196 .map_err(|e| match e.kind { 197 EbicsErrKind::Code { .. } => { 198 failure_status(ErrorCode::END, e, StatusCode::CONFLICT) 199 } 200 _ => failure_status(ErrorCode::END, e, StatusCode::BAD_GATEWAY), 201 })?; 202 203 ApiResult::Ok(Json(SyncSubmit { order })) 204 }, 205 ), 206 ) 207 .with_state(state) 208 .auth(auth, "ebisync") 209 .route( 210 "/config", 211 get(async || { 212 Json(Config { 213 name: (), 214 version: LibtoolVersion::new(0, 0, 0), 215 implementation: Some("urn:net:taler:specs:libeufin-ebbisync:taler-rust"), 216 spa_version: long_version(), 217 }) 218 }), 219 ) 220 }