use crate::{
auth::AuthManager,
config::Config,
database::DatabaseManager,
error::{Result, ZeroTrustError},
types::{HealthStatus, SystemStats},
};
#[cfg(feature = "migration")]
use crate::migration::MigrationManager;
#[cfg(feature = "sync")]
use crate::sync::SyncManager;
use reqwest::{Client, Response};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ZeroTrustClient {
inner: Arc<ClientInner>,
}
#[derive(Debug)]
struct ClientInner {
config: Config,
http_client: Client,
auth: AuthManager,
database: DatabaseManager,
#[cfg(feature = "migration")]
migration: MigrationManager,
#[cfg(feature = "sync")]
sync: SyncManager,
}
impl ZeroTrustClient {
pub async fn new(config: Config) -> Result<Self> {
config.validate()?;
let client_builder = Client::builder()
.user_agent(&config.user_agent)
.timeout(config.timeout)
.danger_accept_invalid_certs(!config.verify_ssl);
let http_client = client_builder.build()?;
let health_url = format!("{}/health", config.api_url);
let response = http_client.get(&health_url).send().await;
match response {
Ok(resp) if resp.status().is_success() => {
}
Ok(resp) => {
return Err(ZeroTrustError::server_error(
resp.status().as_u16(),
format!("Server returned status: {}", resp.status()),
));
}
Err(e) => {
if e.is_connect() {
return Err(ZeroTrustError::generic(format!(
"Could not connect to Zero Trust API at {}. Please check the URL and network connectivity.",
config.api_url
)));
} else if e.is_timeout() {
return Err(ZeroTrustError::Timeout);
} else {
return Err(ZeroTrustError::from(e));
}
}
}
let config_arc = Arc::new(config);
let http_arc = Arc::new(http_client);
let auth = AuthManager::new(config_arc.clone(), http_arc.clone());
let database = DatabaseManager::new(config_arc.clone(), http_arc.clone());
#[cfg(feature = "migration")]
let migration = MigrationManager::new(config_arc.clone(), http_arc.clone());
#[cfg(feature = "sync")]
let sync = SyncManager::new(config_arc.clone(), http_arc.clone());
let inner = ClientInner {
config: (*config_arc).clone(),
http_client: (*http_arc).clone(),
auth,
database,
#[cfg(feature = "migration")]
migration,
#[cfg(feature = "sync")]
sync,
};
Ok(Self {
inner: Arc::new(inner),
})
}
pub async fn with_default_config() -> Result<Self> {
let config = Config::load_default()?;
Self::new(config).await
}
pub fn auth(&self) -> &AuthManager {
&self.inner.auth
}
pub fn databases(&self) -> &DatabaseManager {
&self.inner.database
}
#[cfg(feature = "migration")]
pub fn migration(&self) -> &MigrationManager {
&self.inner.migration
}
#[cfg(feature = "sync")]
pub fn sync(&self) -> &SyncManager {
&self.inner.sync
}
pub async fn health(&self) -> Result<HealthStatus> {
let url = format!("{}/health", self.inner.config.api_url);
let response = self.inner.http_client.get(&url).send().await?;
self.handle_response(response).await
}
pub async fn stats(&self) -> Result<SystemStats> {
let url = format!("{}/api/v1/stats", self.inner.config.api_url);
let response = self.send_authenticated_request(
self.inner.http_client.get(&url)
).await?;
self.handle_response(response).await
}
pub fn config(&self) -> &Config {
&self.inner.config
}
pub fn set_token<S: Into<String>>(&mut self, token: S) {
let mut new_config = self.inner.config.clone();
new_config.token = Some(token.into());
}
pub fn is_authenticated(&self) -> bool {
self.inner.config.is_authenticated()
}
}
impl ZeroTrustClient {
pub(crate) async fn send_authenticated_request(
&self,
mut request: reqwest::RequestBuilder,
) -> Result<Response> {
if let Some(token) = &self.inner.config.token {
request = request.header("Authorization", format!("Bearer {}", token));
}
let response = request
.header("Content-Type", "application/json")
.send()
.await?;
Ok(response)
}
pub(crate) async fn handle_response<T>(&self, response: Response) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let status = response.status();
if status.is_success() {
let data = response.json::<T>().await?;
Ok(data)
} else {
let error_text = response.text().await.unwrap_or_default();
match status.as_u16() {
401 => Err(ZeroTrustError::auth("Authentication failed. Please login again.")),
403 => Err(ZeroTrustError::permission_denied("Insufficient permissions")),
404 => Err(ZeroTrustError::not_found("Resource not found")),
429 => {
let retry_after = 60; Err(ZeroTrustError::rate_limit(retry_after))
}
400..=499 => Err(ZeroTrustError::client_error(status.as_u16(), error_text)),
500..=599 => Err(ZeroTrustError::server_error(status.as_u16(), error_text)),
_ => Err(ZeroTrustError::generic(format!("HTTP {}: {}", status, error_text))),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mockito::{Matcher, Server};
#[tokio::test]
async fn test_client_creation() {
let mut server = Server::new_async().await;
let url = server.url();
let mock = server
.mock("GET", "/health")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"status": "healthy", "version": "0.1.0", "database": "connected", "blockchain": "connected"}"#)
.create_async()
.await;
let config = Config::new(&url).unwrap();
let client = ZeroTrustClient::new(config).await.unwrap();
assert!(!client.is_authenticated());
mock.assert_async().await;
}
#[tokio::test]
async fn test_health_check() {
let mut server = Server::new_async().await;
let url = server.url();
let _init_mock = server
.mock("GET", "/health")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"status": "healthy", "version": "0.1.0", "database": "connected", "blockchain": "connected"}"#)
.create_async()
.await;
let health_mock = server
.mock("GET", "/health")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"status": "healthy", "version": "0.1.0", "database": "connected", "blockchain": "connected"}"#)
.create_async()
.await;
let config = Config::new(&url).unwrap();
let client = ZeroTrustClient::new(config).await.unwrap();
let health = client.health().await.unwrap();
assert_eq!(health.status, "healthy");
assert_eq!(health.version, "0.1.0");
health_mock.assert_async().await;
}
}