| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636 |
- 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
- }
- }
|