Formatting

Signed-off-by: Jacob Kiers <code@kiers.eu>
This commit is contained in:
Jacob Kiers 2024-02-29 22:07:37 +01:00
parent 6255dc1c2f
commit 3de7ca94a2
4 changed files with 290 additions and 278 deletions

View File

@ -1,138 +1,149 @@
use anyhow::anyhow; use anyhow::anyhow;
use isahc::{ReadResponseExt, RequestExt}; use isahc::http::StatusCode;
use isahc::http::StatusCode; use isahc::{ReadResponseExt, RequestExt};
use rsa::pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, LineEnding}; use rsa::pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey, LineEnding};
use rsa::pkcs8::EncodePublicKey; use rsa::pkcs8::EncodePublicKey;
use rsa::RsaPrivateKey; use rsa::RsaPrivateKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{BASE_URL, BunqClient, deserialize_retarded_response, sign}; use crate::{deserialize_retarded_response, sign, BunqClient, BASE_URL};
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
struct AppState { struct AppState {
token: String, token: String,
pem_private: String, pem_private: String,
} }
#[derive(Serialize, Deserialize, Default)] #[derive(Serialize, Deserialize, Default)]
pub struct BunqConfig { pub struct BunqConfig {
api_key: String, api_key: String,
state: Option<AppState>, state: Option<AppState>,
} }
impl BunqConfig { impl BunqConfig {
pub fn load() -> anyhow::Result<BunqConfig> { pub fn load() -> anyhow::Result<BunqConfig> {
println!("Loading config file from {}", confy::get_configuration_file_path("bunq-rs", "bunq-rs")?.to_string_lossy()); println!(
Ok(confy::load("bunq-rs", "bunq-rs")?) "Loading config file from {}",
} confy::get_configuration_file_path("bunq-rs", "bunq-rs")?.to_string_lossy()
pub fn save(&self) -> anyhow::Result<()> { );
println!("Storing config file in {}", confy::get_configuration_file_path("bunq-rs", None)?.to_string_lossy()); Ok(confy::load("bunq-rs", "bunq-rs")?)
confy::store("bunq-rs", "bunq-rs", self)?; }
Ok(()) pub fn save(&self) -> anyhow::Result<()> {
} println!(
pub fn install(mut self) -> anyhow::Result<BunqClient> { "Storing config file in {}",
let api_key = &self.api_key; confy::get_configuration_file_path("bunq-rs", None)?.to_string_lossy()
);
let keypair = if let Some(state) = &self.state { confy::store("bunq-rs", "bunq-rs", self)?;
RsaPrivateKey::from_pkcs1_pem(&state.pem_private)? Ok(())
} else { }
let mut rng = rand::thread_rng(); pub fn install(mut self) -> anyhow::Result<BunqClient> {
let api_key = &self.api_key;
let bits = 2048;
let keypair = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key"); let keypair = if let Some(state) = &self.state {
RsaPrivateKey::from_pkcs1_pem(&state.pem_private)?
let pem_public = keypair.to_public_key().to_public_key_pem(LineEnding::CRLF)?; } else {
let mut rng = rand::thread_rng();
let body = Installation {
client_public_key: &pem_public, let bits = 2048;
}; let keypair = RsaPrivateKey::new(&mut rng, bits).expect("failed to generate a key");
let response = isahc::post(
format!("{}/v1/installation", BASE_URL), let pem_public = keypair
serde_json::to_string(&body)?, .to_public_key()
)? .to_public_key_pem(LineEnding::CRLF)?;
.text()?;
let response: InstallationResponse = deserialize_retarded_response(&response)?.response; let body = Installation {
let token = response.token.token; client_public_key: &pem_public,
};
let body = DeviceServer { let response = isahc::post(
description: "JK Test Server", format!("{}/v1/installation", BASE_URL),
secret: api_key, serde_json::to_string(&body)?,
permitted_ips: &["77.175.86.236", "*"], )?
}; .text()?;
let body = serde_json::to_string(&body)?; let response: InstallationResponse = deserialize_retarded_response(&response)?.response;
let mut response = isahc::http::Request::post(format!("{}/v1/device-server", BASE_URL)) let token = response.token.token;
.header("X-Bunq-Client-Authentication", &token)
.body(body)? let body = DeviceServer {
.send()?; description: "JK Test Server",
secret: api_key,
let response_text = response.text()?; permitted_ips: &["77.175.86.236", "*"],
println!("{}", response_text); };
let body = serde_json::to_string(&body)?;
if response.status() != StatusCode::OK { let mut response = isahc::http::Request::post(format!("{}/v1/device-server", BASE_URL))
return Err(anyhow!(response_text)); .header("X-Bunq-Client-Authentication", &token)
} .body(body)?
.send()?;
self.state = Some(AppState { pem_private: keypair.to_pkcs1_pem(LineEnding::CRLF)?.to_string(), token });
self.save()?; let response_text = response.text()?;
println!("{}", response_text);
keypair
}; if response.status() != StatusCode::OK {
let token = self.state.unwrap().token; return Err(anyhow!(response_text));
let body = SessionServer { secret: api_key }; }
let body = serde_json::to_string(&body)?;
let sig = sign(&body, &keypair)?; self.state = Some(AppState {
let response = isahc::http::Request::post(format!("{}/v1/session-server", BASE_URL)) pem_private: keypair.to_pkcs1_pem(LineEnding::CRLF)?.to_string(),
.header("X-Bunq-Client-Authentication", &token) token,
.header("X-Bunq-Client-Signature", &sig) });
.body(body)? self.save()?;
.send()?
.text()?; keypair
let r: SessionServerResponse = deserialize_retarded_response(&response)?.response; };
let token = self.state.unwrap().token;
Ok(BunqClient { let body = SessionServer { secret: api_key };
keypair, let body = serde_json::to_string(&body)?;
token: r.token.token, let sig = sign(&body, &keypair)?;
user_id: r.user_person.id, let response = isahc::http::Request::post(format!("{}/v1/session-server", BASE_URL))
}) .header("X-Bunq-Client-Authentication", &token)
} .header("X-Bunq-Client-Signature", &sig)
} .body(body)?
.send()?
#[derive(Serialize)] .text()?;
pub(super) struct Installation<'a> { let r: SessionServerResponse = deserialize_retarded_response(&response)?.response;
pub(super) client_public_key: &'a str,
} Ok(BunqClient {
keypair,
#[derive(Deserialize)] token: r.token.token,
pub(super) struct Token { user_id: r.user_person.id,
pub(super) token: String, })
} }
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")] #[derive(Serialize)]
pub(super) struct InstallationResponse { pub(super) struct Installation<'a> {
pub(super) token: Token, pub(super) client_public_key: &'a str,
} }
#[derive(Serialize)] #[derive(Deserialize)]
pub(super) struct DeviceServer<'a> { pub(super) struct Token {
pub(super) description: &'a str, pub(super) token: String,
pub(super) secret: &'a str, }
pub(super) permitted_ips: &'a [&'a str],
} #[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
#[derive(Serialize)] pub(super) struct InstallationResponse {
pub(super) struct SessionServer<'a> { pub(super) token: Token,
pub(super) secret: &'a str, }
}
#[derive(Serialize)]
#[derive(Deserialize)] pub(super) struct DeviceServer<'a> {
#[serde(rename_all = "PascalCase")] pub(super) description: &'a str,
pub(super) struct SessionServerResponse { pub(super) secret: &'a str,
pub(super) token: Token, pub(super) permitted_ips: &'a [&'a str],
pub(super) user_person: UserPerson, }
}
#[derive(Serialize)]
#[derive(Deserialize)] pub(super) struct SessionServer<'a> {
pub(super) struct UserPerson { pub(super) secret: &'a str,
pub(super) id: i64, }
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub(super) struct SessionServerResponse {
pub(super) token: Token,
pub(super) user_person: UserPerson,
}
#[derive(Deserialize)]
pub(super) struct UserPerson {
pub(super) id: i64,
}

View File

@ -1,8 +1,8 @@
use anyhow::Result; use anyhow::Result;
use rsa::pkcs1v15::SigningKey; use rsa::pkcs1v15::SigningKey;
use rsa::RsaPrivateKey;
use rsa::sha2::Sha256; use rsa::sha2::Sha256;
use rsa::signature::RandomizedSigner; use rsa::signature::RandomizedSigner;
use rsa::RsaPrivateKey;
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
pub use config::BunqConfig; pub use config::BunqConfig;
@ -94,7 +94,7 @@ fn deserialize_normal_response<T: DeserializeOwned>(r: &str) -> Result<Response<
} }
fn sign(body: &str, key: &RsaPrivateKey) -> Result<String> { fn sign(body: &str, key: &RsaPrivateKey) -> Result<String> {
use base64::prelude::{BASE64_STANDARD, Engine}; use base64::prelude::{Engine, BASE64_STANDARD};
let signing_key = SigningKey::<Sha256>::new(key.clone()); let signing_key = SigningKey::<Sha256>::new(key.clone());
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();

View File

@ -1,38 +1,38 @@
use isahc::{ReadResponseExt, RequestExt}; use isahc::{ReadResponseExt, RequestExt};
use serde::Deserialize; use serde::Deserialize;
use crate::{BASE_URL, BunqClient}; use crate::{BunqClient, BASE_URL};
pub(super) fn get(client: &BunqClient) -> anyhow::Result<Vec<MonetaryAccount>> { pub(super) fn get(client: &BunqClient) -> anyhow::Result<Vec<MonetaryAccount>> {
let response = isahc::http::Request::get(format!( let response = isahc::http::Request::get(format!(
"{}/v1/user/{}/monetary-account", "{}/v1/user/{}/monetary-account",
BASE_URL, client.user_id BASE_URL, client.user_id
)) ))
.header("X-Bunq-Client-Authentication", &client.token) .header("X-Bunq-Client-Authentication", &client.token)
.body(())? .body(())?
.send()? .send()?
.text()?; .text()?;
let accounts = crate::deserialize_normal_response::<Vec<MonetaryAccount>>(&response)?; let accounts = crate::deserialize_normal_response::<Vec<MonetaryAccount>>(&response)?;
Ok(accounts.response) Ok(accounts.response)
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")] #[serde(rename_all = "PascalCase")]
pub enum MonetaryAccount { pub enum MonetaryAccount {
MonetaryAccountBank(MonetaryAccountBank), MonetaryAccountBank(MonetaryAccountBank),
MonetaryAccountSavings(MonetaryAccountSavings), MonetaryAccountSavings(MonetaryAccountSavings),
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct MonetaryAccountBank { pub struct MonetaryAccountBank {
pub id: i64, pub id: i64,
pub description: String, pub description: String,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct MonetaryAccountSavings { pub struct MonetaryAccountSavings {
pub id: i64, pub id: i64,
pub description: String, pub description: String,
} }

View File

@ -1,100 +1,101 @@
use anyhow::Result; use anyhow::Result;
use isahc::{ReadResponseExt, RequestExt}; use isahc::{ReadResponseExt, RequestExt};
use serde::Deserialize; use serde::Deserialize;
use crate::{BASE_URL, BunqClient, MonetaryAccount, Pagination, Response}; use crate::{BunqClient, MonetaryAccount, Pagination, Response, BASE_URL};
pub(super) fn get_from_to(client: &BunqClient, pub(super) fn get_from_to(
acc: &MonetaryAccount, client: &BunqClient,
from: Option<i64>, acc: &MonetaryAccount,
to: Option<i64>) -> Result<Vec<Payment>> from: Option<i64>,
{ to: Option<i64>,
let next_page = |url: &str| -> Result<(_, _)> { ) -> Result<Vec<Payment>> {
let response = isahc::http::Request::get(url) let next_page = |url: &str| -> Result<(_, _)> {
.header("X-Bunq-Client-Authentication", &client.token) let response = isahc::http::Request::get(url)
.body(())? .header("X-Bunq-Client-Authentication", &client.token)
.send()? .body(())?
.text()?; .send()?
let Response { .text()?;
response, let Response {
pagination, response,
} = crate::deserialize_normal_response::<Vec<PaymentPayment>>(&response)?; pagination,
Ok(( } = crate::deserialize_normal_response::<Vec<PaymentPayment>>(&response)?;
response.into_iter().map(|p| p.payment).collect(), Ok((
pagination, response.into_iter().map(|p| p.payment).collect(),
)) pagination,
}; ))
};
let account_id = match acc {
MonetaryAccount::MonetaryAccountBank(bank) => bank.id, let account_id = match acc {
MonetaryAccount::MonetaryAccountSavings(savings) => savings.id, MonetaryAccount::MonetaryAccountBank(bank) => bank.id,
}; MonetaryAccount::MonetaryAccountSavings(savings) => savings.id,
};
let mut url = format!(
"/v1/user/{}/monetary-account/{}/payment", let mut url = format!(
client.user_id, account_id "/v1/user/{}/monetary-account/{}/payment",
); client.user_id, account_id
);
if let Some(to) = to {
url = format!("{}?newer_id={}", url, to); if let Some(to) = to {
} url = format!("{}?newer_id={}", url, to);
}
let mut all = Vec::new();
loop { let mut all = Vec::new();
let (mut payments, pag) = next_page(&format!("{}{}", BASE_URL, url))?; loop {
dbg!(&payments); let (mut payments, pag) = next_page(&format!("{}{}", BASE_URL, url))?;
all.append(&mut payments); dbg!(&payments);
dbg!(&pag); all.append(&mut payments);
if let Some(Pagination { dbg!(&pag);
older_url: Some(older_url), if let Some(Pagination {
.. older_url: Some(older_url),
}) = pag ..
{ }) = pag
if let (Some(latest), Some(from)) = (all.last(), from) { {
if latest.id <= from { if let (Some(latest), Some(from)) = (all.last(), from) {
all.retain(|p| p.id >= from); if latest.id <= from {
break; all.retain(|p| p.id >= from);
} break;
} }
url = older_url; }
} else { url = older_url;
break; } else {
} break;
} }
Ok(all) }
} Ok(all)
}
#[derive(Deserialize, Debug)]
pub struct LabelMonetaryAccount { #[derive(Deserialize, Debug)]
pub iban: Option<String>, pub struct LabelMonetaryAccount {
pub display_name: String, pub iban: Option<String>,
pub merchant_category_code: Option<String>, pub display_name: String,
} pub merchant_category_code: Option<String>,
}
#[derive(Deserialize, Debug)]
pub struct Amount { #[derive(Deserialize, Debug)]
pub value: String, pub struct Amount {
pub currency: String, pub value: String,
} pub currency: String,
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")] #[derive(Deserialize, Debug)]
struct PaymentPayment { #[serde(rename_all = "PascalCase")]
payment: Payment, struct PaymentPayment {
} payment: Payment,
}
#[derive(Deserialize, Debug)]
pub struct Payment { #[derive(Deserialize, Debug)]
pub alias: LabelMonetaryAccount, pub struct Payment {
pub counterparty_alias: LabelMonetaryAccount, pub alias: LabelMonetaryAccount,
pub amount: Amount, pub counterparty_alias: LabelMonetaryAccount,
pub balance_after_mutation: Amount, pub amount: Amount,
pub created: String, pub balance_after_mutation: Amount,
pub updated: String, pub created: String,
pub description: String, pub updated: String,
pub id: i64, pub description: String,
pub monetary_account_id: i64, pub id: i64,
#[serde(rename = "type")] pub monetary_account_id: i64,
pub type_: String, #[serde(rename = "type")]
pub sub_type: String, pub type_: String,
} pub sub_type: String,
}