use crate::error::{CliError, Result};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct ApiClient {
base_url: String,
token: Option<String>,
client: reqwest::Client,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginRequest {
pub grant_type: String,
pub username: String,
pub password: String,
pub client_id: String,
pub client_secret: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct LoginResponse {
pub access_token: String,
pub token_type: String,
pub expires_in: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_token: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UserInfo {
pub id: String,
pub username: String,
pub email: String,
pub role: String,
pub status: String,
pub tenant_id: String,
pub created_at: String,
pub updated_at: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email_verified: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub external_user_id: Option<String>,
}
impl ApiClient {
pub fn new(base_url: String, token: Option<String>) -> Result<Self> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.map_err(|e| CliError::NetworkError(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
base_url,
token,
client,
})
}
pub fn set_token(&mut self, token: String) {
self.token = Some(token);
}
fn build_headers(&self) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
if let Some(token) = &self.token {
let auth_value = format!("Bearer {}", token);
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&auth_value)
.map_err(|e| CliError::AuthError(format!("Invalid token: {}", e)))?,
);
}
Ok(headers)
}
pub async fn login(&self, username: &str, password: &str) -> Result<LoginResponse> {
let url = format!("{}/api/v1/oauth2/token", self.base_url);
let body = serde_json::json!({
"grant_type": "password",
"username": username,
"password": password,
"client_id": "oauth-db-cli",
"client_secret": "cli-secret",
});
let response = self
.client
.post(&url)
.json(&body)
.send()
.await
.map_err(|e| CliError::NetworkError(format!("Login request failed: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(CliError::AuthError(format!(
"Login failed ({}): {}",
status, error_text
)));
}
let login_response: LoginResponse = response
.json()
.await
.map_err(|e| CliError::SerializationError(format!("Failed to parse response: {}", e)))?;
Ok(login_response)
}
pub async fn get_me(&self) -> Result<UserInfo> {
let url = format!("{}/api/v1/users/me", self.base_url);
let headers = self.build_headers()?;
let response = self
.client
.get(&url)
.headers(headers)
.send()
.await
.map_err(|e| CliError::NetworkError(format!("Request failed: {}", e)))?;
if response.status() == 401 {
return Err(CliError::NotLoggedIn);
}
if !response.status().is_success() {
let status = response.status();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(CliError::ApiError(format!(
"Request failed ({}): {}",
status, error_text
)));
}
let user_info: UserInfo = response
.json()
.await
.map_err(|e| CliError::SerializationError(format!("Failed to parse response: {}", e)))?;
Ok(user_info)
}
pub async fn verify_token(&self) -> Result<bool> {
match self.get_me().await {
Ok(_) => Ok(true),
Err(CliError::NotLoggedIn) => Ok(false),
Err(e) => Err(e),
}
}
pub async fn get<T: serde::de::DeserializeOwned>(
&self,
path: &str,
query: &[(&str, String)],
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let headers = self.build_headers()?;
let mut request = self.client.get(&url).headers(headers);
if !query.is_empty() {
request = request.query(query);
}
let response = request
.send()
.await
.map_err(|e| CliError::NetworkError(format!("Request failed: {}", e)))?;
self.handle_response(response).await
}
pub async fn post<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &serde_json::Value,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let headers = self.build_headers()?;
let response = self
.client
.post(&url)
.headers(headers)
.json(body)
.send()
.await
.map_err(|e| CliError::NetworkError(format!("Request failed: {}", e)))?;
self.handle_response(response).await
}
pub async fn patch<T: serde::de::DeserializeOwned>(
&self,
path: &str,
body: &serde_json::Value,
) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let headers = self.build_headers()?;
let response = self
.client
.patch(&url)
.headers(headers)
.json(body)
.send()
.await
.map_err(|e| CliError::NetworkError(format!("Request failed: {}", e)))?;
self.handle_response(response).await
}
pub async fn delete<T: serde::de::DeserializeOwned>(&self, path: &str) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let headers = self.build_headers()?;
let response = self
.client
.delete(&url)
.headers(headers)
.send()
.await
.map_err(|e| CliError::NetworkError(format!("Request failed: {}", e)))?;
self.handle_response(response).await
}
async fn handle_response<T: serde::de::DeserializeOwned>(
&self,
response: reqwest::Response,
) -> Result<T> {
let status = response.status();
if status == 401 {
return Err(CliError::NotLoggedIn);
}
if !status.is_success() {
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Unknown error".to_string());
return Err(match status.as_u16() {
403 => CliError::PermissionDenied(error_text),
404 => CliError::NotFound(error_text),
409 => CliError::InvalidInput(format!("Conflict: {}", error_text)),
422 => CliError::InvalidInput(error_text),
429 => CliError::ApiError(format!("Rate limit exceeded: {}", error_text)),
_ => CliError::ApiError(format!("Request failed ({}): {}", status, error_text)),
});
}
response
.json()
.await
.map_err(|e| CliError::SerializationError(format!("Failed to parse response: {}", e)))
}
}