libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }