diff options
Diffstat (limited to 'src/api.rs')
| -rw-r--r-- | src/api.rs | 636 |
1 files changed, 636 insertions, 0 deletions
diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..0321103 --- /dev/null +++ b/src/api.rs @@ -0,0 +1,636 @@ +use anyhow::{Context, Result}; +use reqwest::{blocking::{Client, Response}, header}; +use serde_json::json; +use std::sync::{atomic::{AtomicBool, Ordering}, Arc}; +use std::time::Duration; + +use crate::models::*; + +/// API Client for BeepZone backend +#[derive(Clone)] +pub struct ApiClient { + client: Client, + base_url: String, + token: Option<String>, + db_timeout_flag: Arc<AtomicBool>, +} + +impl ApiClient { + /// Create a new API client + pub fn new(base_url: String) -> Result<Self> { + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .context("Failed to create HTTP client")?; + + Ok(Self { + client, + base_url: base_url.trim_end_matches('/').to_string(), + token: None, + db_timeout_flag: Arc::new(AtomicBool::new(false)), + }) + } + + fn flag_timeout_signal(&self) { + self.db_timeout_flag.store(true, Ordering::SeqCst); + } + + fn observe_response_error(&self, error: &Option<String>) { + if Self::is_database_timeout_error(error) { + self.flag_timeout_signal(); + } + } + + fn send_request( + &self, + builder: reqwest::blocking::RequestBuilder, + context_msg: &'static str, + ) -> Result<Response> { + builder + .send() + .map_err(|err| { + self.flag_timeout_signal(); + err + }) + .context(context_msg) + } + + /// Returns true if a timeout signal was previously raised (and clears it) + pub fn take_timeout_signal(&self) -> bool { + self.db_timeout_flag.swap(false, Ordering::SeqCst) + } + + /// Set the authentication token + pub fn set_token(&mut self, token: String) { + self.token = Some(token); + } + + /// Clear the authentication token + #[allow(dead_code)] + pub fn clear_token(&mut self) { + self.token = None; + } + + /// Check if server is reachable + pub fn health_check(&self) -> Result<bool> { + let url = format!("{}/health", self.base_url); + let response = self + .send_request(self.client.get(&url), "Failed to perform health check")?; + Ok(response.status().is_success()) + } + + /// Get health details (tries to parse JSON; returns None if non-JSON) + pub fn health_info(&self) -> Result<Option<serde_json::Value>> { + let url = format!("{}/health", self.base_url); + let response = self + .send_request(self.client.get(&url), "Failed to fetch health info")?; + if !response.status().is_success() { + return Ok(None); + } + // Try to parse as JSON; if it fails, just return None (some servers return plain text) + let text = response.text()?; + match serde_json::from_str::<serde_json::Value>(&text) { + Ok(v) => Ok(Some(v)), + Err(_) => Ok(None), + } + } + + /// Check if the error message indicates a database timeout + pub fn is_database_timeout_error(error: &Option<String>) -> bool { + if let Some(err) = error { + err.contains("Database temporarily unavailable") + } else { + false + } + } + + // Authentication Methods + + /// Login with username and password + pub fn login_password(&self, username: &str, password: &str) -> Result<LoginResponse> { + let url = format!("{}/auth/login", self.base_url); + let body = LoginRequest { + method: "password".to_string(), + username: Some(username.to_string()), + password: Some(password.to_string()), + pin: None, + login_string: None, + }; + + let response = self.send_request( + self.client.post(&url).json(&body), + "Failed to send login request", + )?; + + let result: LoginResponse = response.json().context("Failed to parse login response")?; + + Ok(result) + } + + /// Login with PIN + #[allow(dead_code)] + pub fn login_pin(&self, username: &str, pin: &str) -> Result<ApiResponse<LoginResponse>> { + let url = format!("{}/auth/login", self.base_url); + let body = LoginRequest { + method: "pin".to_string(), + username: Some(username.to_string()), + password: None, + pin: Some(pin.to_string()), + login_string: None, + }; + + let response = self.send_request( + self.client.post(&url).json(&body), + "Failed to send PIN login request", + )?; + + let result: ApiResponse<LoginResponse> = + response.json().context("Failed to parse login response")?; + self.observe_response_error(&result.error); + + Ok(result) + } + + /// Login with token/RFID string + #[allow(dead_code)] + pub fn login_token(&self, login_string: &str) -> Result<ApiResponse<LoginResponse>> { + let url = format!("{}/auth/login", self.base_url); + let body = LoginRequest { + method: "token".to_string(), + username: None, + password: None, + pin: None, + login_string: Some(login_string.to_string()), + }; + + let response = self.send_request( + self.client.post(&url).json(&body), + "Failed to send token login request", + )?; + + let result: ApiResponse<LoginResponse> = + response.json().context("Failed to parse login response")?; + self.observe_response_error(&result.error); + + Ok(result) + } + + /// Logout current session + pub fn logout(&self) -> Result<ApiResponse<()>> { + let url = format!("{}/auth/logout", self.base_url); + let response = self.send_request( + self.make_authorized_request(reqwest::Method::POST, &url)?, + "Failed to send logout request", + )?; + + let result: ApiResponse<()> = response.json()?; + self.observe_response_error(&result.error); + Ok(result) + } + + /// Check session status + #[allow(dead_code)] + pub fn check_session(&self) -> Result<ApiResponse<SessionStatus>> { + let url = format!("{}/auth/status", self.base_url); + let response = self.send_request( + self.make_authorized_request(reqwest::Method::GET, &url)?, + "Failed to check session status", + )?; + + let result: ApiResponse<SessionStatus> = response.json()?; + self.observe_response_error(&result.error); + Ok(result) + } + + /// Best-effort session validity check. + /// Returns Ok(true) when the session appears valid, Ok(false) when clearly invalid (401/403 or explicit valid=false). + /// Be tolerant of different backend response shapes and assume valid on ambiguous 2xx responses. + pub fn check_session_valid(&self) -> Result<bool> { + let url = format!("{}/auth/status", self.base_url); + let response = self.send_request( + self.make_authorized_request(reqwest::Method::GET, &url)?, + "Failed to check session status", + )?; + + let status = response.status(); + let text = response.text()?; + + // Explicitly invalid if unauthorized/forbidden + if status.as_u16() == 401 || status.as_u16() == 403 { + return Ok(false); + } + + // Parse generic JSON and look for common shapes first + if let Ok(val) = serde_json::from_str::<serde_json::Value>(&text) { + // data.valid + if let Some(valid) = val + .get("data") + .and_then(|d| d.get("valid")) + .and_then(|v| v.as_bool()) + { + return Ok(valid); + } + // top-level valid + if let Some(valid) = val.get("valid").and_then(|v| v.as_bool()) { + return Ok(valid); + } + // success=true is generally a good sign + if val.get("success").and_then(|v| v.as_bool()) == Some(true) { + return Ok(true); + } + } + + // As a last attempt, try strict ApiResponse<SessionStatus> + if let Ok(parsed) = serde_json::from_str::<ApiResponse<SessionStatus>>(&text) { + if let Some(data) = parsed.data { + return Ok(data.valid); + } + // If no data provided, treat success=true as valid by default + return Ok(parsed.success || status.is_success()); + } + + // Last resort: if response was 2xx and not explicitly invalid, assume valid + Ok(status.is_success()) + } + + // Permissions & Preferences + + /// Get current user's permissions + #[allow(dead_code)] + pub fn get_permissions(&self) -> Result<ApiResponse<PermissionsResponse>> { + let url = format!("{}/permissions", self.base_url); + let response = self.send_request( + self.make_authorized_request(reqwest::Method::GET, &url)?, + "Failed to get permissions", + )?; + + let result: ApiResponse<PermissionsResponse> = response.json()?; + self.observe_response_error(&result.error); + Ok(result) + } + + /// Get user preferences + #[allow(dead_code)] + pub fn get_preferences( + &self, + user_id: Option<i32>, + ) -> Result<ApiResponse<PreferencesResponse>> { + let url = format!("{}/preferences", self.base_url); + let body = PreferencesRequest { + action: "get".to_string(), + user_id, + preferences: None, + }; + + let response = self.send_request( + self.make_authorized_request(reqwest::Method::POST, &url)? + .json(&body), + "Failed to get preferences", + )?; + + let result: ApiResponse<PreferencesResponse> = response.json()?; + self.observe_response_error(&result.error); + Ok(result) + } + + /// Set user preferences + #[allow(dead_code)] + pub fn set_preferences( + &self, + values: serde_json::Value, + user_id: Option<i32>, + ) -> Result<ApiResponse<PreferencesResponse>> { + let url = format!("{}/preferences", self.base_url); + let body = PreferencesRequest { + action: "set".to_string(), + user_id, + preferences: Some(values), + }; + + let response = self.send_request( + self.make_authorized_request(reqwest::Method::POST, &url)? + .json(&body), + "Failed to set preferences", + )?; + + let result: ApiResponse<PreferencesResponse> = response.json()?; + self.observe_response_error(&result.error); + Ok(result) + } + + // Query Methods + + /// Execute a generic query + pub fn query(&self, request: &QueryRequest) -> Result<ApiResponse<serde_json::Value>> { + let url = format!("{}/query", self.base_url); + + // Log the serialized request for debugging + let body = serde_json::to_value(request)?; + log::debug!("Query request JSON: {}", serde_json::to_string(&body)?); + + // Log the request for debugging JOINs + if request.joins.is_some() { + log::debug!( + "Query with JOINs: table={}, columns={:?}, joins={:?}", + request.table, + request.columns.as_ref().map(|c| c.len()), + request.joins.as_ref().map(|j| j.len()) + ); + } + + let response = self.send_request( + self.make_authorized_request(reqwest::Method::POST, &url)? + .json(&body), + "Failed to execute query", + )?; + + // Try to get the response text for debugging + let status = response.status(); + let response_text = response.text()?; + + // Log the raw response for debugging + if !status.is_success() { + log::error!("API error ({}): {}", status, response_text); + } else { + log::debug!( + "API response (first 500 chars): {}", + if response_text.len() > 500 { + &response_text[..500] + } else { + &response_text + } + ); + } + + // Now try to parse it + let result: ApiResponse<serde_json::Value> = serde_json::from_str(&response_text) + .with_context(|| { + format!( + "Failed to parse API response. Status: {}, Body: {}", + status, + if response_text.len() > 200 { + &response_text[..200] + } else { + &response_text + } + ) + })?; + self.observe_response_error(&result.error); + + Ok(result) + } + + /// Select records from a table + pub fn select( + &self, + table: &str, + columns: Option<Vec<String>>, + where_clause: Option<serde_json::Value>, + order_by: Option<Vec<OrderBy>>, + limit: Option<u32>, + ) -> Result<ApiResponse<Vec<serde_json::Value>>> { + let request = QueryRequest { + action: "select".to_string(), + table: table.to_string(), + columns, + data: None, + r#where: where_clause, + filter: None, + order_by, + limit, + offset: None, + joins: None, + }; + + let response = self.query(&request)?; + if response.success { + let data = response.data.unwrap_or(json!([])); + let records: Vec<serde_json::Value> = serde_json::from_value(data)?; + Ok(ApiResponse { + success: true, + data: Some(records), + error: None, + message: response.message, + }) + } else { + Ok(ApiResponse { + success: false, + data: None, + error: response.error, + message: response.message, + }) + } + } + + /// Select records from a table with JOINs + pub fn select_with_joins( + &self, + table: &str, + columns: Option<Vec<String>>, + where_clause: Option<serde_json::Value>, + filter: Option<serde_json::Value>, + order_by: Option<Vec<OrderBy>>, + limit: Option<u32>, + joins: Option<Vec<Join>>, + ) -> Result<ApiResponse<Vec<serde_json::Value>>> { + let request = QueryRequest { + action: "select".to_string(), + table: table.to_string(), + columns, + data: None, + r#where: where_clause, + filter, + order_by, + limit, + offset: None, + joins, + }; + + let response = self.query(&request)?; + if response.success { + let data = response.data.unwrap_or(json!([])); + let records: Vec<serde_json::Value> = serde_json::from_value(data)?; + Ok(ApiResponse { + success: true, + data: Some(records), + error: None, + message: response.message, + }) + } else { + Ok(ApiResponse { + success: false, + data: None, + error: response.error, + message: response.message, + }) + } + } + + /// Insert a record + pub fn insert(&self, table: &str, values: serde_json::Value) -> Result<ApiResponse<i32>> { + let request = QueryRequest { + action: "insert".to_string(), + table: table.to_string(), + columns: None, + data: Some(values), + r#where: None, + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + let response = self.query(&request)?; + if response.success { + let id: i32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; + Ok(ApiResponse { + success: true, + data: Some(id), + error: None, + message: response.message, + }) + } else { + Ok(ApiResponse { + success: false, + data: None, + error: response.error, + message: response.message, + }) + } + } + + /// Update records + pub fn update( + &self, + table: &str, + values: serde_json::Value, + where_clause: serde_json::Value, + ) -> Result<ApiResponse<u32>> { + let request = QueryRequest { + action: "update".to_string(), + table: table.to_string(), + columns: None, + data: Some(values), + r#where: Some(where_clause), + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + let response = self.query(&request)?; + if response.success { + let count: u32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; + Ok(ApiResponse { + success: true, + data: Some(count), + error: None, + message: response.message, + }) + } else { + Ok(ApiResponse { + success: false, + data: None, + error: response.error, + message: response.message, + }) + } + } + + /// Delete records + pub fn delete(&self, table: &str, where_clause: serde_json::Value) -> Result<ApiResponse<u32>> { + let request = QueryRequest { + action: "delete".to_string(), + table: table.to_string(), + columns: None, + data: None, + r#where: Some(where_clause), + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + let response = self.query(&request)?; + if response.success { + let count: u32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; + Ok(ApiResponse { + success: true, + data: Some(count), + error: None, + message: response.message, + }) + } else { + Ok(ApiResponse { + success: false, + data: None, + error: response.error, + message: response.message, + }) + } + } + + /// Cunt records + pub fn count( + &self, + table: &str, + where_clause: Option<serde_json::Value>, + ) -> Result<ApiResponse<i32>> { + let request = QueryRequest { + action: "count".to_string(), + table: table.to_string(), + columns: None, + data: None, + r#where: where_clause, + filter: None, + order_by: None, + limit: None, + offset: None, + joins: None, + }; + + let response = self.query(&request)?; + if response.success { + let count: i32 = serde_json::from_value(response.data.unwrap_or(json!(0)))?; + Ok(ApiResponse { + success: true, + data: Some(count), + error: None, + message: response.message, + }) + } else { + Ok(ApiResponse { + success: false, + data: None, + error: response.error, + message: response.message, + }) + } + } + + // Helper Methods + + /// Create an authorized request with proper headers + fn make_authorized_request( + &self, + method: reqwest::Method, + url: &str, + ) -> Result<reqwest::blocking::RequestBuilder> { + let token = self.token.as_ref().context("No authentication token set")?; + + let builder = self + .client + .request(method, url) + .header(header::AUTHORIZATION, format!("Bearer {}", token)); + + Ok(builder) + } + + /// Get the based URL + pub fn base_url(&self) -> &str { + &self.base_url + } +} |
