taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

commit 7797b39802f6254f87bd15ae1909396daea74113
parent 72832a7d9de80cc40a8307f0f66152d45ccfa77d
Author: Antoine A <>
Date:   Fri, 17 Jan 2025 15:03:19 +0100

magnet-bank: setup reset and dev cmd

Diffstat:
MCargo.lock | 13+++++++------
MCargo.toml | 1+
Mwire-gateway/magnet-bank/Cargo.toml | 4++--
Awire-gateway/magnet-bank/src/dev.rs | 46++++++++++++++++++++++++++++++++++++++++++++++
Mwire-gateway/magnet-bank/src/keys.rs | 48++++++++++++++++++++++++++++++++++++++++++------
Mwire-gateway/magnet-bank/src/lib.rs | 2++
Mwire-gateway/magnet-bank/src/magnet.rs | 76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwire-gateway/magnet-bank/src/magnet/error.rs | 43+++++++++++++++++++++++--------------------
Mwire-gateway/magnet-bank/src/main.rs | 19++++++++++++++-----
9 files changed, 213 insertions(+), 39 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -304,9 +304,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.9" +version = "1.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" dependencies = [ "shlex", ] @@ -1392,9 +1392,9 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jiff" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7597657ea66d53f6e926a67d4cc3d125c4b57fa662f2d007a5476307de948453" +checksum = "d2bb0c2e28117985a4d90e3bc70092bc8f226f434c7ec7e23dd9ff99c5c5721a" dependencies = [ "log", "portable-atomic", @@ -1498,6 +1498,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "serde_path_to_error", "serde_urlencoded", "sha1", "spki", @@ -2973,9 +2974,9 @@ checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" [[package]] name = "valuable" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "vcpkg" diff --git a/Cargo.toml b/Cargo.toml @@ -14,6 +14,7 @@ debug = true thiserror = "2.0" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } +serde_path_to_error = "0.1" tokio = { version = "1.42", features = ["macros"] } axum = "0.8" sqlx = { version = "0.8", default-features = false } diff --git a/wire-gateway/magnet-bank/Cargo.toml b/wire-gateway/magnet-bank/Cargo.toml @@ -30,6 +30,7 @@ taler-common.workspace = true taler-api.workspace = true clap.workspace = true serde.workspace = true +serde_path_to_error.workspace = true thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true @@ -37,4 +38,4 @@ tokio.workspace = true [dev-dependencies] -test-utils.workspace = true -\ No newline at end of file +test-utils.workspace = true diff --git a/wire-gateway/magnet-bank/src/dev.rs b/wire-gateway/magnet-bank/src/dev.rs @@ -0,0 +1,46 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ + +use sqlx::PgPool; +use taler_common::config::Config; + +use crate::{ + config::{DbConfig, MagnetConfig}, + keys, + magnet::AuthClient, +}; + +#[derive(clap::Subcommand, Debug)] +pub enum DevCmd { + /// Print account info + Account, +} + +pub async fn dev(cfg: Config, cmd: DevCmd) -> anyhow::Result<()> { + let db = DbConfig::parse(&cfg)?; + let pool = PgPool::connect_with(db.cfg).await?; + let cfg = MagnetConfig::parse(&cfg)?; + let keys = keys::load(&cfg)?; + let client = AuthClient::new(reqwest::Client::new(), cfg.api_url, cfg.consumer) + .upgrade(keys.access_token); + match cmd { + DevCmd::Account => { + let res = client.list_accounts().await?; + dbg!(res); + } + } + Ok(()) +} diff --git a/wire-gateway/magnet-bank/src/keys.rs b/wire-gateway/magnet-bank/src/keys.rs @@ -29,18 +29,54 @@ use crate::{ }; #[derive(Default, Debug, serde::Deserialize, serde::Serialize)] -pub struct MagnetKeys { +struct KeysFile { access_token: Option<Token>, signing_key: Option<Base32<32>>, } -pub async fn setup(cfg: MagnetConfig) -> Result<(), anyhow::Error> { +#[derive(Debug)] +pub struct Keys { + pub access_token: Token, + pub signing_key: SigningKey, +} + +pub fn load(cfg: &MagnetConfig) -> anyhow::Result<Keys> { + // Load JSON file + let file: KeysFile = match json_file::load(&cfg.keys_path) { + Ok(file) => file, + Err(e) => return Err(anyhow::anyhow!("Could not magnet keys: {e}")), + }; + + fn incomplete_err() -> anyhow::Error { + anyhow::anyhow!("Missing magnet keys, run 'magnet-bank setup' first") + } + + // Check full + let access_token = file.access_token.ok_or_else(incomplete_err)?; + let signing_key = file.signing_key.ok_or_else(incomplete_err)?; + + // Load signing key + + let signing_key = SigningKey::from_slice(&*signing_key)?; + + Ok(Keys { + access_token, + signing_key, + }) +} + +pub async fn setup(cfg: MagnetConfig, reset: bool) -> anyhow::Result<()> { + if reset { + if let Err(e) = std::fs::remove_file(&cfg.keys_path) { + if e.kind() != ErrorKind::NotFound { + Err(e)?; + } + } + } let mut keys = match json_file::load(&cfg.keys_path) { Ok(existing) => existing, - Err(e) => match e.kind() { - ErrorKind::NotFound => MagnetKeys::default(), - _ => Err(e)?, - }, + Err(e) if e.kind() == ErrorKind::NotFound => KeysFile::default(), + Err(e) => Err(e)?, }; let client = AuthClient::new( diff --git a/wire-gateway/magnet-bank/src/lib.rs b/wire-gateway/magnet-bank/src/lib.rs @@ -20,3 +20,4 @@ pub mod db; pub mod keys; pub mod magnet; pub mod wire_gateway; +pub mod dev; +\ No newline at end of file diff --git a/wire-gateway/magnet-bank/src/magnet.rs b/wire-gateway/magnet-bank/src/magnet.rs @@ -19,6 +19,7 @@ use error::ApiResult; use p256::{ecdsa::SigningKey, PublicKey}; use serde_json::{json, Value}; use spki::EncodePublicKey; +use taler_common::types::amount; use crate::magnet::{error::MagnetBuilder, oauth::OAuthBuilder}; @@ -80,6 +81,72 @@ pub struct ScaResult { pub sent_to: Vec<String>, } +#[derive(serde::Deserialize, Debug)] +pub struct Partner { + #[serde(rename = "megnevezes")] + pub name: String, + #[serde(rename = "kod")] + pub code: u64, + #[serde(rename = "adoszam")] + pub tax_number: Option<String>, + #[serde(rename = "ebUfallapot")] + pub status: String, // TODO enum +} + +#[derive(serde::Deserialize, Debug)] +pub struct AccountType { + #[serde(rename = "kod")] + code: u64, + #[serde(rename = "megnevezes")] + name: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct Currency { + #[serde(rename = "jel")] + symbol: amount::Currency, + #[serde(rename = "megnevezes")] + name: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct Account { + #[serde(rename = "alapertelmezett")] + default: bool, + #[serde(rename = "bankszamlaTipus")] + ty: AccountType, + #[serde(rename = "deviza")] + currency: Currency, + #[serde(rename = "ibanSzamlaszam")] + iban: String, + #[serde(rename = "kod")] + code: u64, + #[serde(rename = "szamlaszam")] + number: String, + #[serde(rename = "tulajdonosKod")] + owner_code: u64, + #[serde(rename = "lakossagi")] + resident: bool, + #[serde(rename = "megnevezes")] + name: Option<String>, + partner: Partner, +} + +#[derive(serde::Deserialize, Debug)] +pub struct PartnerAccounts { + partner: Partner, + #[serde(rename = "bankszamlaList")] + bank_accounts: Vec<Account>, + #[serde(rename = "kertJogosultsag")] + requested_permission: u64, +} + +#[derive(serde::Deserialize, Debug)] +pub struct PartnerList { + #[serde(rename = "partnerSzamlaList")] + parteners: Vec<PartnerAccounts>, +} + pub struct AuthClient { client: reqwest::Client, api_url: reqwest::Url, @@ -191,4 +258,13 @@ impl ApiClient { .magnet_json() .await } + + pub async fn list_accounts(&self) -> ApiResult<PartnerList> { + self.client + .get(self.join("/RESTApi/resources/v2/partnerszamla/0")) + .oauth(&self.consumer, Some(&self.access), None) + .await + .magnet_json() + .await + } } diff --git a/wire-gateway/magnet-bank/src/magnet/error.rs b/wire-gateway/magnet-bank/src/magnet/error.rs @@ -16,15 +16,24 @@ use reqwest::{header, Response, StatusCode}; use serde::{de::DeserializeOwned, Deserialize}; -use serde_json::value::RawValue; use thiserror::Error; use tracing::error; #[derive(Deserialize, Debug)] -pub struct MagnetHeader { +pub struct MagnetResponse<T> { timestamp: jiff::civil::DateTime, - #[serde(alias = "errorCode")] - error_code: Option<u16>, + #[serde(flatten)] + body: MagnetBody<T>, +} + +#[derive(Deserialize, Debug)] +pub struct Empty {} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +pub enum MagnetBody<T> { + Error(MagnetError), + Ok(T), } #[derive(Deserialize, Error, Debug)] @@ -45,7 +54,7 @@ pub enum ApiError { #[error("magnet {0}")] Magnet(#[from] MagnetError), #[error("JSON body: {0}")] - Json(#[from] serde_json::Error), + Json(#[from] serde_path_to_error::Error<serde_json::Error>), #[error("form body: {0}")] Form(#[from] serde_urlencoded::de::Error), #[error("status {0}")] @@ -117,22 +126,16 @@ async fn error_handling(res: reqwest::Result<Response>) -> ApiResult<String> { } /** Parse magnet JSON response */ -async fn magnet_raw_json(res: reqwest::Result<Response>) -> ApiResult<Box<RawValue>> { +async fn magnet_json<T: DeserializeOwned>(res: reqwest::Result<Response>) -> ApiResult<T> { let body = error_handling(res).await?; - let raw = RawValue::from_string(body).map_err(ApiError::Json)?; - let header: MagnetHeader = serde_json::from_str(raw.get()).map_err(ApiError::Json)?; - if header.error_code.is_some_and(|it| it != 200) { - let error: MagnetError = serde_json::from_str(raw.get()).map_err(ApiError::Json)?; - Err(ApiError::Magnet(error)) - } else { - Ok(raw) - } -} + let deserializer = &mut serde_json::Deserializer::from_str(&body); -/** Parse magnet JSON response into our own type */ -async fn magnet_json<T: DeserializeOwned>(response: reqwest::Result<Response>) -> ApiResult<T> { - let raw = magnet_raw_json(response).await?; - serde_json::from_str(raw.get()).map_err(ApiError::Json) + let body: MagnetResponse<T> = + serde_path_to_error::deserialize(deserializer).map_err(ApiError::Json)?; + match body.body { + MagnetBody::Error(e) => Err(ApiError::Magnet(e)), + MagnetBody::Ok(t) => Ok(t), + } } /** Parse magnet URL encoded response into our own type */ @@ -158,7 +161,7 @@ impl MagnetBuilder for reqwest::Result<Response> { } async fn magnet_empty(self) -> ApiResult<()> { - magnet_raw_json(self).await?; + magnet_json::<Empty>(self).await?; Ok(()) } diff --git a/wire-gateway/magnet-bank/src/main.rs b/wire-gateway/magnet-bank/src/main.rs @@ -19,7 +19,9 @@ use std::{future::Future, path::PathBuf, sync::Arc}; use clap::Parser; use magnet_bank::{ config::{DbConfig, MagnetConfig, WireGatewayConfig}, - db, keys, + db, + dev::{self, DevCmd}, + keys, wire_gateway::MagnetWireGateway, }; use sqlx::PgPool; @@ -50,7 +52,10 @@ struct Args { #[derive(clap::Subcommand, Debug)] enum Command { /// Setup magnet-bank auth token and account settings for Wire Gateway use - Setup, + Setup { + #[clap(long, short)] + reset: bool, + }, /// Initialize magnet-bank database Dbinit { #[clap(long, short)] @@ -58,6 +63,9 @@ enum Command { }, /// Run magnet-bank HTTP server Serve, + /// Hidden dev commands + #[command(subcommand, hide(true))] + Dev(DevCmd), } fn setup(level: Option<tracing::Level>, app: impl Future<Output = Result<(), anyhow::Error>>) { @@ -83,15 +91,15 @@ fn setup(level: Option<tracing::Level>, app: impl Future<Output = Result<(), any } } -async fn app(args: Args) -> Result<(), anyhow::Error> { +async fn app(args: Args) -> anyhow::Result<()> { let source = ConfigSource::new("magnet-bank", "magnet-bank", "magnet-bank"); let cfg = Config::from_file(source, args.config)?; match args.cmd { - Command::Setup => { + Command::Setup { reset } => { let cfg = MagnetConfig::parse(&cfg)?; - keys::setup(cfg).await? + keys::setup(cfg, reset).await? } Command::Dbinit { reset } => { let db = DbConfig::parse(&cfg)?; @@ -111,6 +119,7 @@ async fn app(args: Args) -> Result<(), anyhow::Error> { ) .await?; } + Command::Dev(dev_cmd) => dev::dev(cfg, dev_cmd).await?, } Ok(()) }