diff --git a/.env.example b/.env.example index 4d85982..7e80c02 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,4 @@ URL=https://b2b.antares.hu/YOUR_BASE_URI_HERE ANTARES_USERCODE=given_usercode ANTARES_PASSWORD=given_password -OUT=out\\test.xlsx +OUT=out\\test.xlsx \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 9f45236..f21e59f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,7 @@ dependencies = [ "rust_xlsxwriter", "serde", "serde_json", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e11d808..86e5ded 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,4 +11,4 @@ rust_xlsxwriter = "0.66" dotenv = "0.15" chrono = "0.4" log = "0.4" - +zeroize = "1.5" diff --git a/README.md b/README.md index 92dc747..604af65 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Octopus ERP Converter](docs/images/Octopus8.ico) -Rust utility to fetch product data from Antares B2B and export to Excel. +Rust utility to fetch product data from Antares B2B and export to Excel which can be imported to Octopus 8. @@ -22,9 +22,10 @@ cargo build --release `.env` file: ```env -ANTARES_USERCODE=your_usercode -ANTARES_PASSWORD=your_password_hash -OUT=out +URL=https://b2b.antares.hu/YOUR_BASE_URI_HERE +ANTARES_USERCODE=given_usercode +ANTARES_PASSWORD=given_password +OUT=out\\test.xlsx ``` `OUT` can be: @@ -68,4 +69,4 @@ cargo test ## License -MIT License © 2026 Orink Hungary Kft. +MIT [License](./LICENSE) © 2026 Orink Hungary Kft. diff --git a/src/api/client.rs b/src/api/client.rs index 34a8d04..50115e2 100644 --- a/src/api/client.rs +++ b/src/api/client.rs @@ -1,6 +1,10 @@ -pub fn make_url(base: &str, usercode: &str, password: &str, cikkszam: &str) -> String { +use crate::template::antares::AntaresLogin; + +pub fn make_url(login: AntaresLogin) -> String { format!( - "{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam={}", - base, usercode, password, cikkszam + "{}&USERCODE={}&PASSWORD={}&PIN=&cikkszam=", + login.url, + login.usercode, + login.password.as_str() ) } diff --git a/src/main.rs b/src/main.rs index d0922f7..230bb4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,24 +2,22 @@ mod api; mod template; mod tools; -use reqwest::blocking::Client; +use zeroize::Zeroizing; use std::{ error::Error, fs, - time::Duration, io::{ Error as IO_Error, - ErrorKind::{ - Other, - ConnectionRefused - } - } + ErrorKind::Other + }, + path::PathBuf }; use api::make_url; -use template::antares::Antares; +use template::antares::{AntaresLogin, Antares}; use tools::{ Logger, get_env, + make_request, export_to_excel }; @@ -28,68 +26,41 @@ const USERCODE_KEY: &str = "ANTARES_USERCODE"; const PASSWORD_KEY: &str = "ANTARES_PASSWORD"; const OUT_KEY: &str = "OUT"; -fn make_request(url: &str, logger: &Logger) -> Result> { - let client = Client::builder() - .timeout(Duration::from_secs(300)) - .build().map_err(|e| { - logger.log_error(&format!("Failed to create HTTP client: {}", e)); - Box::new(IO_Error::new(Other, e.to_string())) - })?; - - let response = client.get(url).send().map_err(|e| { - logger.log_error(&format!("HTTP request failed: {}", e)); - Box::new(IO_Error::new(ConnectionRefused, e.to_string())) - })?; - - if response.status().is_success() { - Ok(response.text().map_err(|e| { - logger.log_error(&format!("Failed to read response body: {}", e)); - Box::new(IO_Error::new(Other, e.to_string())) - })?) - } else { - let error_msg = format!("HTTP error: {:?}", response.status()); - logger.log_error(&error_msg); - Err(Box::new(IO_Error::new(ConnectionRefused, error_msg))) - } -} - fn main() -> Result<(), Box> { let logger = Logger::init()?; - logger.log_info("Starting Antares data export"); + logger.log("Starting Antares data export"); dotenv::dotenv().ok(); - let usercode = get_env(USERCODE_KEY) - .map_err(|e| { - logger.log_error(&e); - e - })?; - - let password = get_env(PASSWORD_KEY) - .map_err(|e| { - logger.log_error(&e); - e - })?; + let antares_login = AntaresLogin{ + url: get_env(URL_KEY) + .map_err(|e| { + logger.log(&e); + e + })?, + usercode: get_env(USERCODE_KEY) + .map_err(|e| { + logger.log(&e); + e + })?, + password: Zeroizing::new( + get_env(PASSWORD_KEY) + .map_err(|e| { + logger.log(&e); + e + })? + ) + }; let out_path = get_env(OUT_KEY) .map_err(|e| { - logger.log_error(&e); + logger.log(&e); e })?; - let url = get_env(URL_KEY) - .map_err(|e| { - logger.log_error(&e); - e - })?; - - let cikkszam = ""; // supply a value if needed; - - let url = make_url(&url, &usercode, &password, cikkszam); - - let response = match make_request(&url, &logger) { + let response = match make_request(&make_url(antares_login), &logger) { Ok(resp) => { - logger.log_info("API request sent successfully"); + logger.log("API request sent successfully"); resp } Err(e) => { @@ -98,10 +69,11 @@ fn main() -> Result<(), Box> { }; // Ensure `temp/` exists and prepare path for `antares.json`. - let temp_dir = "temp"; - let antares_file = format!("{}/antares.json", temp_dir); - fs::create_dir_all(temp_dir).map_err(|e| { - logger.log_error(&format!("Failed to create temp directory '{}': {}", temp_dir, e)); + let temp_dir = PathBuf::from("temp"); + let antares_file = PathBuf::from(temp_dir.clone()) + .join("antares.json"); + fs::create_dir_all(&temp_dir).map_err(|e| { + logger.log(&format!("Failed to create temp directory '{:#?}': {}", temp_dir, e)); Box::new(IO_Error::new(Other, e.to_string())) })?; @@ -112,12 +84,12 @@ fn main() -> Result<(), Box> { let pretty = match serde_json::to_string_pretty(&items) { Ok(pretty) => pretty, Err(e) => { - logger.log_error(&format!("Failed to serialize items: {}", e)); + logger.log(&format!("Failed to serialize items: {}", e)); return Err(Box::new(e)); } }; fs::write(&antares_file, &pretty).map_err(|e| { - logger.log_error(&format!("Failed to write {}: {}", antares_file, e)); + logger.log(&format!("Failed to write {:#?}: {}", antares_file, e)); Box::new(IO_Error::new(Other, e.to_string())) })?; logger.log(&format!("API call successful - Fetched {} items", items.len())); @@ -129,21 +101,21 @@ fn main() -> Result<(), Box> { let pretty = match serde_json::to_string_pretty(&json_val) { Ok(pretty) => pretty, Err(e) => { - logger.log_error(&format!("Failed to serialize JSON: {}", e)); + logger.log(&format!("Failed to serialize JSON: {}", e)); return Err(Box::new(e)); } }; fs::write(&antares_file, &pretty).map_err(|e| { - logger.log_error(&format!("Failed to write {}: {}", antares_file, e)); + logger.log(&format!("Failed to write {:#?}: {}", antares_file, e)); Box::new(IO_Error::new(Other, e.to_string())) })?; - logger.log_info("Response saved as JSON (schema mismatch)"); + logger.log("Response saved as JSON (schema mismatch)"); } else { fs::write(&antares_file, &response).map_err(|e| { - logger.log_error(&format!("Failed to write {}: {}", antares_file, e)); + logger.log(&format!("Failed to write {:#?}: {}", antares_file, e)); Box::new(IO_Error::new(Other, e.to_string())) })?; - logger.log_info("Response saved as raw text (not valid JSON)"); + logger.log("Response saved as raw text (not valid JSON)"); } Vec::new() } @@ -155,11 +127,11 @@ fn main() -> Result<(), Box> { logger.log(&format!("Export successful - {} rows exported to {}", items.len(), out_path)); } Err(e) => { - logger.log_error(&format!("Export failed: {}", e)); + logger.log(&format!("Export failed: {}", e)); return Err(e); } } - logger.log_info("Export process completed successfully"); + logger.log("Export process completed successfully"); Ok(()) } diff --git a/src/template/antares.rs b/src/template/antares.rs index bb3759b..2379fcf 100644 --- a/src/template/antares.rs +++ b/src/template/antares.rs @@ -1,6 +1,13 @@ //! Generated structs for Antares JSON response - use serde::{Deserialize, Serialize}; +use zeroize::Zeroizing; + +#[derive(Clone)] +pub struct AntaresLogin { + pub url: String, + pub usercode: String, + pub password: Zeroizing +} #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "PascalCase")] diff --git a/src/tools/logger.rs b/src/tools/logger.rs index be7900d..d309bd7 100644 --- a/src/tools/logger.rs +++ b/src/tools/logger.rs @@ -1,6 +1,13 @@ -use std::fs; -use std::io::Write; -use std::path::PathBuf; +// Logger module +use std::{ + io::Result as IO_Result, + fs::{ + OpenOptions, + create_dir_all + }, + io::Write, + path::PathBuf +}; use chrono::Local; pub struct Logger { @@ -8,9 +15,9 @@ pub struct Logger { } impl Logger { - pub fn init() -> std::io::Result { + pub fn init() -> IO_Result { let log_dir = PathBuf::from("log"); - fs::create_dir_all(&log_dir)?; + create_dir_all(&log_dir)?; let today = Local::now().format("%Y-%m-%d").to_string(); let log_file = log_dir.join(format!("{}.log", today)); @@ -24,7 +31,7 @@ impl Logger { println!("{}", formatted); - if let Ok(mut file) = fs::OpenOptions::new() + if let Ok(mut file) = OpenOptions::new() .create(true) .append(true) .open(&self.log_file) @@ -32,12 +39,4 @@ impl Logger { let _ = writeln!(file, "{}", formatted); } } - - pub fn log_info(&self, message: &str) { - self.log(&format!("ℹ {}", message)); - } - - pub fn log_error(&self, error: &str) { - self.log(&format!("✗ Error: {}", error)); - } } diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 0c01bbb..0d493aa 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,7 +1,9 @@ pub mod excel; pub mod logger; pub mod env; +pub mod request; -pub use excel::export_to_excel; pub use logger::Logger; -pub use env::get_env; \ No newline at end of file +pub use env::get_env; +pub use request::make_request; +pub use excel::export_to_excel; diff --git a/src/tools/request.rs b/src/tools/request.rs new file mode 100644 index 0000000..ea0949a --- /dev/null +++ b/src/tools/request.rs @@ -0,0 +1,46 @@ +// Request module +use crate::tools::Logger; +use std::{ + error::Error, + time::Duration, + io::{ + Error as IO_Error, ErrorKind::{ + ConnectionRefused, Other + } + } +}; +use reqwest::blocking::Client; + + +pub fn get_client() -> Result { + Client::builder() + .timeout(Duration::from_secs(600)) + .build() +} + + +pub fn make_request(url: &str, logger: &Logger) -> Result> { + let client = get_client() + .map_err(|e| { + logger.log(&format!("Failed to create HTTP client: {}", e)); + Box::new(IO_Error::new(Other, e.to_string())) + })?; + + let response = client.get(url) + .send() + .map_err(|e| { + logger.log(&format!("HTTP request failed: {}", e)); + Box::new(IO_Error::new(ConnectionRefused, e.to_string())) + })?; + + if response.status().is_success() { + Ok(response.text().map_err(|e| { + logger.log(&format!("Failed to read response body: {}", e)); + Box::new(IO_Error::new(ConnectionRefused, e.to_string())) + })?) + } else { + let error_msg = format!("HTTP error: {:?}", response.status()); + logger.log(&error_msg); + Err(Box::new(IO_Error::new(ConnectionRefused, error_msg))) + } +}