feat(config): proxy
Make HTTP client proxy configurable for both paheko and helloasso
This commit is contained in:
parent
19117e2a19
commit
40c41fd930
4 changed files with 148 additions and 69 deletions
7
TODO.md
7
TODO.md
|
@ -20,9 +20,14 @@ we should have created our own platform so people can register and pay later (ei
|
|||
|
||||
TODO:
|
||||
- more configuration options
|
||||
- configurable proxy
|
||||
- summary of the operations at the end of run
|
||||
- how many users were added, muted?
|
||||
- conjoined user: add attached member to paheko
|
||||
- "Membre lié"
|
||||
- is this kind of thing even accessible on the API-level ?
|
||||
|
||||
- better error handling & report to the user
|
||||
- handle import error
|
||||
- handle name of the service or service fee not found
|
||||
|
||||
- BUG: quand l'utilisateur est déjà créé, ya un problème d'ID, le user summary n'a pas le bon id, il faut le populer depuis ce qu'on a déjà fetch
|
||||
|
|
|
@ -19,11 +19,23 @@ struct WebSession {
|
|||
jwt: String
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[fully_pub]
|
||||
struct Client {
|
||||
client: reqwest::Client,
|
||||
struct ClientConfig {
|
||||
base_url: Url,
|
||||
proxy: Option<reqwest::Proxy>,
|
||||
user_agent: String
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
ClientConfig {
|
||||
proxy: None,
|
||||
base_url: Url::parse("https://api.helloasso.com/v5/") // the traling slash is important
|
||||
.expect("Expected valid helloasso API base URL"),
|
||||
user_agent: "".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
|
@ -33,31 +45,47 @@ struct LoginPayload {
|
|||
password: String
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct Client {
|
||||
client: reqwest::Client,
|
||||
config: ClientConfig
|
||||
}
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
let base_config: ClientConfig = Default::default();
|
||||
Client {
|
||||
client: Client::get_base_client_builder()
|
||||
client: Client::get_base_client_builder(&base_config)
|
||||
.build()
|
||||
.expect("reqwest client to be built"),
|
||||
base_url: Url::parse("https://api.helloasso.com/v5/")
|
||||
.expect("Valid helloasso API base URL")
|
||||
.expect("Expected reqwest client to be built"),
|
||||
config: base_config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
|
||||
fn get_base_client_builder() -> reqwest::ClientBuilder {
|
||||
impl Client {
|
||||
pub fn new(config: ClientConfig) -> Client {
|
||||
Client {
|
||||
client: Client::get_base_client_builder(&config)
|
||||
.build()
|
||||
.expect("Expected reqwest client to be built"),
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
fn get_base_client_builder(config: &ClientConfig) -> reqwest::ClientBuilder {
|
||||
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||
default_headers.insert("Accept", "application/json".parse().unwrap());
|
||||
// decoy user agent
|
||||
default_headers.insert("User-Agent", "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0".parse().unwrap());
|
||||
default_headers.insert("User-Agent", config.user_agent.parse().unwrap());
|
||||
|
||||
// TODO: configurable proxy
|
||||
// let proxy = reqwest::Proxy::https("https://localhost:8999").unwrap();
|
||||
reqwest::Client::builder()
|
||||
// .proxy(proxy)
|
||||
.default_headers(default_headers)
|
||||
let mut builder = reqwest::Client::builder()
|
||||
.default_headers(default_headers);
|
||||
if let Some(proxy) = &config.proxy {
|
||||
builder = builder.proxy(proxy.clone());
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
pub async fn login(&mut self, payload: LoginPayload) -> Result<AuthentifiedClient> {
|
||||
|
@ -67,12 +95,12 @@ impl Client {
|
|||
"https://auth.helloasso.com".parse().expect("Header value to be OK")
|
||||
);
|
||||
|
||||
let res = self.client.get(self.base_url.join("auth/antiforgerytoken")?)
|
||||
let res = self.client.get(self.config.base_url.join("auth/antiforgerytoken")?)
|
||||
.headers(login_commons_headers.clone())
|
||||
.send().await?;
|
||||
let antiforgerytoken: String = res.json().await?;
|
||||
|
||||
let res = self.client.post(self.base_url.join("auth/login")?)
|
||||
let res = self.client.post(self.config.base_url.join("auth/login")?)
|
||||
.json(&payload)
|
||||
.headers(login_commons_headers.clone())
|
||||
.header("x-csrf-token", antiforgerytoken)
|
||||
|
@ -107,7 +135,7 @@ impl Client {
|
|||
}
|
||||
|
||||
pub fn authentified_client(&self, session: WebSession) -> AuthentifiedClient {
|
||||
AuthentifiedClient::new(self.base_url.clone(), session)
|
||||
AuthentifiedClient::new(self.config.clone(), session)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +144,7 @@ impl Client {
|
|||
struct AuthentifiedClient {
|
||||
session: WebSession,
|
||||
client: reqwest::Client,
|
||||
base_url: Url
|
||||
config: ClientConfig
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
@ -167,30 +195,30 @@ struct UserDetails {
|
|||
|
||||
impl AuthentifiedClient {
|
||||
/// each time we need to change the token, we will need to rebuild the client
|
||||
pub fn new(base_url: Url, session: WebSession) -> Self {
|
||||
pub fn new(config: ClientConfig, session: WebSession) -> Self {
|
||||
let mut auth_headers = reqwest::header::HeaderMap::new();
|
||||
auth_headers.insert("Authorization", format!("Bearer {}", session.jwt).parse().unwrap());
|
||||
|
||||
AuthentifiedClient {
|
||||
base_url,
|
||||
session,
|
||||
client: Client::get_base_client_builder()
|
||||
client: Client::get_base_client_builder(&config)
|
||||
.default_headers(auth_headers)
|
||||
.build()
|
||||
.expect("reqwest client to be built")
|
||||
.expect("reqwest client to be built"),
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn verify_auth(&self) -> Result<bool> {
|
||||
let res = self.client
|
||||
.get(self.base_url.join("agg/user")?)
|
||||
.get(self.config.base_url.join("agg/user")?)
|
||||
.send().await?;
|
||||
Ok(res.status() == 200)
|
||||
}
|
||||
|
||||
pub async fn get_user_details(&self) -> Result<serde_json::Value> {
|
||||
let res = self.client
|
||||
.get(self.base_url.join("agg/user")?)
|
||||
.get(self.config.base_url.join("agg/user")?)
|
||||
.send().await?;
|
||||
if res.status() != 200 {
|
||||
return Err(APIClientError::InvalidStatusCode.into());
|
||||
|
@ -202,7 +230,7 @@ impl AuthentifiedClient {
|
|||
|
||||
async fn simple_fetch(&self, path: String) -> Result<serde_json::Value> {
|
||||
let res = self.client
|
||||
.get(self.base_url.join(path.as_str())?)
|
||||
.get(self.config.base_url.join(path.as_str())?)
|
||||
.send().await?;
|
||||
if res.status() != 200 {
|
||||
return Err(APIClientError::InvalidStatusCode.into());
|
||||
|
@ -217,7 +245,7 @@ impl AuthentifiedClient {
|
|||
let mut continuation_token: Option<String> = None;
|
||||
|
||||
loop {
|
||||
let mut url = self.base_url.join(path.as_str())?;
|
||||
let mut url = self.config.base_url.join(path.as_str())?;
|
||||
if let Some(token) = &continuation_token {
|
||||
url.query_pairs_mut().append_pair("continuationToken", token);
|
||||
}
|
||||
|
|
35
src/main.rs
35
src/main.rs
|
@ -3,22 +3,26 @@ mod paheko;
|
|||
mod helloasso;
|
||||
|
||||
use thiserror::Error;
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use chrono::prelude::{NaiveDate, DateTime, Utc, Datelike};
|
||||
use strum::Display;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use utils::generate_id;
|
||||
use url::Url;
|
||||
|
||||
/// permanent config to store long-term config
|
||||
/// used to ingest env settings
|
||||
/// config loaded from env variables
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct Config {
|
||||
helloasso_proxy: Option<String>,
|
||||
helloasso_email: String,
|
||||
helloasso_password: String,
|
||||
paheko_proxy: Option<String>,
|
||||
paheko_base_url: String,
|
||||
paheko_client_id: String,
|
||||
paheko_client_secret: String,
|
||||
paheko_accounting_year_id: u64,
|
||||
}
|
||||
|
||||
// start user cache management
|
||||
|
@ -40,6 +44,7 @@ enum LoadError {
|
|||
FailedToWrite
|
||||
}
|
||||
|
||||
const APP_USER_AGENT: &str = "helloasso_paheko_adapter";
|
||||
|
||||
fn write_user_cache(cache: &UserCache) -> Result<(), LoadError> {
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix(env!("CARGO_PKG_NAME"))
|
||||
|
@ -175,6 +180,14 @@ fn parse_and_get_birthday_year(raw_date: String) -> Option<u32> {
|
|||
d.year().try_into().ok()
|
||||
}
|
||||
|
||||
fn get_proxy_from_url(proxy_url: Option<String>) -> Result<Option<reqwest::Proxy>> {
|
||||
Ok(match proxy_url {
|
||||
Some(p) => Some(reqwest::Proxy::all(&p)
|
||||
.context("Expected to build Proxy from paheko_proxy config value")?),
|
||||
None => None
|
||||
})
|
||||
}
|
||||
|
||||
async fn launch_adapter() -> Result<()> {
|
||||
dotenvy::dotenv()?;
|
||||
|
||||
|
@ -182,7 +195,14 @@ async fn launch_adapter() -> Result<()> {
|
|||
|
||||
let mut user_cache = load_user_cache().context("Failed to load user cache")?;
|
||||
|
||||
let mut paheko_client: paheko::Client = paheko::Client::new(config.paheko_base_url);
|
||||
if !&config.paheko_base_url.ends_with("/") {
|
||||
return Err(anyhow!("Invalid paheko base_url, it must end with a slash"))
|
||||
}
|
||||
let mut paheko_client: paheko::Client = paheko::Client::new(paheko::ClientConfig {
|
||||
base_url: Url::parse(&config.paheko_base_url).expect("Expected paheko base url to be a valid URL"),
|
||||
proxy: get_proxy_from_url(config.paheko_proxy)?,
|
||||
user_agent: APP_USER_AGENT.to_string()
|
||||
});
|
||||
|
||||
let paheko_credentials = paheko::Credentials {
|
||||
client_id: config.paheko_client_id,
|
||||
|
@ -190,8 +210,12 @@ async fn launch_adapter() -> Result<()> {
|
|||
};
|
||||
let paheko_client: paheko::AuthentifiedClient = paheko_client.login(paheko_credentials).await?;
|
||||
|
||||
|
||||
let mut ha_client: helloasso::Client = Default::default();
|
||||
let mut ha_client: helloasso::Client = helloasso::Client::new(helloasso::ClientConfig {
|
||||
base_url: Url::parse("https://api.helloasso.com/v5/")
|
||||
.expect("Expected valid helloasso API base URL"),
|
||||
proxy: get_proxy_from_url(config.helloasso_proxy)?,
|
||||
user_agent: "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/112.0".to_string()
|
||||
});
|
||||
|
||||
let login_payload = helloasso::LoginPayload {
|
||||
email: config.helloasso_email,
|
||||
|
@ -378,6 +402,7 @@ async fn launch_adapter() -> Result<()> {
|
|||
|
||||
// add transaction
|
||||
let transaction = paheko::SimpleTransaction {
|
||||
accounting_year: utils::Id(config.paheko_accounting_year_id),
|
||||
// TODO: make the label template configurable
|
||||
label: format!("Adhésion {:?} via HelloAsso", pk_membership.mode_name),
|
||||
amount: pk_membership.payed_amount,
|
||||
|
@ -392,7 +417,7 @@ async fn launch_adapter() -> Result<()> {
|
|||
linked_services: pk_user_service_registrations.iter().map(|x| x.id.clone()).collect()
|
||||
};
|
||||
let _ = paheko_client.register_transaction(transaction)
|
||||
.await.context("Expected to create new paheko transaction");
|
||||
.await.context("Expected to create new paheko transaction")?;
|
||||
eprintln!(" Created paheko transaction");
|
||||
|
||||
pk_memberships.push(pk_membership);
|
||||
|
|
|
@ -92,17 +92,10 @@ struct SimpleTransaction {
|
|||
debit_account_code: String,
|
||||
reference: String,
|
||||
linked_users: Vec<Id>,
|
||||
linked_services: Vec<Id>
|
||||
linked_services: Vec<Id>,
|
||||
accounting_year: Id
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct Client {
|
||||
client: reqwest::Client,
|
||||
base_url: Url,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum APIClientError {
|
||||
#[error("Received non-normal status code from API")]
|
||||
|
@ -116,15 +109,40 @@ struct Credentials {
|
|||
client_secret: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[fully_pub]
|
||||
struct ClientConfig {
|
||||
base_url: Url,
|
||||
proxy: Option<reqwest::Proxy>,
|
||||
user_agent: String
|
||||
}
|
||||
|
||||
impl Default for ClientConfig {
|
||||
fn default() -> Self {
|
||||
ClientConfig {
|
||||
proxy: None,
|
||||
base_url: Url::parse("https://paheko.example.org/api/") // the traling slash is important
|
||||
.expect("Expected valid paheko API base URL"),
|
||||
user_agent: "".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[fully_pub]
|
||||
struct Client {
|
||||
client: reqwest::Client,
|
||||
config: ClientConfig
|
||||
}
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
let base_config: ClientConfig = Default::default();
|
||||
Client {
|
||||
client: Client::get_base_client_builder()
|
||||
client: Client::get_base_client_builder(&base_config)
|
||||
.build()
|
||||
.expect("Expected reqwest client to be built"),
|
||||
base_url: Url::parse("https://paheko.etoiledebethleem.fr/api/") // the traling slash is important
|
||||
.expect("Expected valid paheko API base URL")
|
||||
config: base_config
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,25 +161,26 @@ fn build_auth_headers(credentials: &Credentials) -> reqwest::header::HeaderMap {
|
|||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(base_url: String) -> Client {
|
||||
pub fn new(config: ClientConfig) -> Client {
|
||||
Client {
|
||||
client: Client::get_base_client_builder()
|
||||
client: Client::get_base_client_builder(&config)
|
||||
.build()
|
||||
.expect("Expected reqwest client to be built"),
|
||||
base_url: Url::parse(&base_url) // the traling slash is important
|
||||
.expect("Expected valid paheko API base URL")
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
fn get_base_client_builder() -> reqwest::ClientBuilder {
|
||||
fn get_base_client_builder(config: &ClientConfig) -> reqwest::ClientBuilder {
|
||||
let mut default_headers = reqwest::header::HeaderMap::new();
|
||||
default_headers.insert("Accept", "application/json".parse().unwrap());
|
||||
default_headers.insert("User-Agent", "helloasso_paheko_adapter".parse().unwrap());
|
||||
default_headers.insert("User-Agent", config.user_agent.parse().unwrap());
|
||||
|
||||
// let proxy = reqwest::Proxy::http("http://localhost:8998").unwrap();
|
||||
reqwest::Client::builder()
|
||||
// .proxy(proxy)
|
||||
.default_headers(default_headers)
|
||||
let mut builder = reqwest::Client::builder()
|
||||
.default_headers(default_headers);
|
||||
if let Some(proxy) = &config.proxy {
|
||||
builder = builder.proxy(proxy.clone());
|
||||
}
|
||||
builder
|
||||
}
|
||||
|
||||
pub async fn login(&mut self, credentials: Credentials) -> Result<AuthentifiedClient> {
|
||||
|
@ -181,16 +200,16 @@ impl Client {
|
|||
}
|
||||
|
||||
pub fn authentified_client(&self, credentials: Credentials) -> AuthentifiedClient {
|
||||
AuthentifiedClient::new(self.base_url.clone(), credentials)
|
||||
AuthentifiedClient::new(self.config.clone(), credentials)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AuthentifiedClient {
|
||||
credentials: Credentials,
|
||||
_credentials: Credentials,
|
||||
client: reqwest::Client,
|
||||
base_url: Url
|
||||
config: ClientConfig
|
||||
}
|
||||
|
||||
// SELECT id,nom AS first_name,last_name,email,external_custom_data FROM users LIMIT 5;
|
||||
|
@ -225,14 +244,14 @@ struct UserServiceRegistration {
|
|||
}
|
||||
|
||||
impl AuthentifiedClient {
|
||||
pub fn new(base_url: Url, credentials: Credentials) -> Self {
|
||||
pub fn new(config: ClientConfig, credentials: Credentials) -> Self {
|
||||
AuthentifiedClient {
|
||||
client: Client::get_base_client_builder()
|
||||
client: Client::get_base_client_builder(&config)
|
||||
.default_headers(build_auth_headers(&credentials))
|
||||
.build()
|
||||
.expect("Expect client to be built"),
|
||||
credentials,
|
||||
base_url
|
||||
_credentials: credentials,
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -242,7 +261,7 @@ impl AuthentifiedClient {
|
|||
sql: String
|
||||
}
|
||||
let payload = Payload { sql: query };
|
||||
let path = self.base_url.join("sql")?;
|
||||
let path = self.config.base_url.join("sql")?;
|
||||
let res = self.client
|
||||
.post(path)
|
||||
.json(&payload)
|
||||
|
@ -329,7 +348,7 @@ impl AuthentifiedClient {
|
|||
.part("file", part);
|
||||
|
||||
let res = self.client
|
||||
.post(self.base_url.join("user/import/")?)
|
||||
.post(self.config.base_url.join("user/import/")?)
|
||||
.multipart(form)
|
||||
.send().await?;
|
||||
|
||||
|
@ -377,7 +396,7 @@ impl AuthentifiedClient {
|
|||
.part("file", part);
|
||||
|
||||
let res = self.client
|
||||
.post(self.base_url.join("services/subscriptions/import")?)
|
||||
.post(self.config.base_url.join("services/subscriptions/import")?)
|
||||
.multipart(form)
|
||||
.send().await?;
|
||||
|
||||
|
@ -395,25 +414,27 @@ impl AuthentifiedClient {
|
|||
use reqwest::multipart::Form;
|
||||
|
||||
let mut form = Form::new()
|
||||
.text("id_year", "1")
|
||||
.text("id_year", transaction.accounting_year.to_string())
|
||||
.text("label", transaction.label)
|
||||
.text("date", transaction.inception_time.format("%d/%m/%Y").to_string())
|
||||
.text("type", Into::<String>::into(transaction.kind))
|
||||
.text("amount", format!("{}", transaction.amount))
|
||||
.text("debit", transaction.debit_account_code)
|
||||
.text("credit", transaction.credit_account_code)
|
||||
// "Numéro pièce comptable" enregistré au niveau de la transaction
|
||||
.text("reference", transaction.reference)
|
||||
;
|
||||
|
||||
for linked_id in transaction.linked_users {
|
||||
form = form.text("linked_users[]", format!("{}", linked_id.0));
|
||||
}
|
||||
// only possible with paheko fork
|
||||
for linked_id in transaction.linked_services {
|
||||
form = form.text("linked_services[]", format!("{}", linked_id.0));
|
||||
}
|
||||
|
||||
let res = self.client
|
||||
.post(self.base_url.join("accounting/transaction")?)
|
||||
.post(self.config.base_url.join("accounting/transaction")?)
|
||||
.multipart(form)
|
||||
.send().await?;
|
||||
|
||||
|
|
Loading…
Reference in a new issue