oauth-db-cli 0.1.0

Command-line tool for managing OAuth-DB platform
Documentation
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)
    }

    /// Login to the platform
    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)
    }

    /// Get current user information
    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)
    }

    /// Verify if the token is still valid
    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),
        }
    }

    /// Generic GET request
    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
    }

    /// Generic POST request
    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
    }

    /// Generic PATCH request
    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
    }

    /// Generic DELETE request
    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
    }

    /// Handle HTTP response and parse JSON
    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)))
    }
}