xjp-oidc 1.1.0

OIDC/OAuth2 SDK for Rust - Server and WASM support
Documentation
//! HTTP client extension for multi-tenant support

use crate::{
    errors::Result,
    http::{HttpClient, HttpClientError},
};
use async_trait::async_trait;
use serde_json::Value;

/// Enhanced HTTP client trait with admin header support for multi-tenant
#[async_trait]
pub trait HttpClientWithAdminSupport: Send + Sync {
    /// Perform a GET request with optional admin override header
    async fn get_value_with_admin_override(
        &self,
        url: &str,
        admin_tenant: Option<&str>,
    ) -> Result<Value>;
}

/// Adapter to add admin header support to existing HttpClient implementations
pub struct HttpClientAdapter<T: HttpClient> {
    inner: T,
}

impl<T: HttpClient> HttpClientAdapter<T> {
    /// Create a new adapter wrapping an existing HTTP client
    pub fn new(client: T) -> Self {
        Self { inner: client }
    }
}

#[async_trait]
impl<T: HttpClient> HttpClientWithAdminSupport for HttpClientAdapter<T> {
    async fn get_value_with_admin_override(
        &self,
        url: &str,
        admin_tenant: Option<&str>,
    ) -> Result<Value> {
        // For basic adapter, admin override is not supported
        // This will just use the standard client
        if admin_tenant.is_some() {
            tracing::warn!(
                "Admin tenant override requested but not supported by underlying client: {:?}",
                admin_tenant
            );
        }
        self.inner
            .get_value(url)
            .await
            .map_err(|e| crate::errors::Error::Network(e.to_string()))
    }
}

/// Enhanced Reqwest implementation with header support
#[cfg(all(not(target_arch = "wasm32"), feature = "http-reqwest"))]
#[cfg_attr(docsrs, doc(cfg(all(not(target_arch = "wasm32"), feature = "http-reqwest"))))]
pub mod reqwest_tenant {
    use super::*;
    use reqwest::{Client, header::{HeaderMap, HeaderName, HeaderValue}};
    use std::time::Duration;

    /// Enhanced Reqwest HTTP client with admin override support
    #[derive(Clone)]
    pub struct ReqwestHttpClientWithAdminSupport {
        client: Client,
        admin_key: Option<String>,
    }

    impl ReqwestHttpClientWithAdminSupport {
        /// Create a new HTTP client with default settings
        pub fn new() -> Result<Self> {
            let client = Client::builder()
                .timeout(Duration::from_secs(30))
                .use_rustls_tls()
                .build()
                .map_err(|e| crate::errors::Error::Network(e.to_string()))?;

            Ok(Self { client, admin_key: None })
        }

        /// Create a new HTTP client with admin key support
        pub fn with_admin_key(admin_key: String) -> Result<Self> {
            let client = Client::builder()
                .timeout(Duration::from_secs(30))
                .use_rustls_tls()
                .build()
                .map_err(|e| crate::errors::Error::Network(e.to_string()))?;

            Ok(Self { client, admin_key: Some(admin_key) })
        }

        /// Create a new HTTP client with custom timeout
        pub fn with_timeout(timeout_secs: u64) -> Result<Self> {
            let client = Client::builder()
                .timeout(Duration::from_secs(timeout_secs))
                .use_rustls_tls()
                .build()
                .map_err(|e| crate::errors::Error::Network(e.to_string()))?;

            Ok(Self { client, admin_key: None })
        }
    }

    impl Default for ReqwestHttpClientWithAdminSupport {
        fn default() -> Self {
            Self::new().expect("Failed to create default HTTP client")
        }
    }

    #[async_trait]
    impl HttpClientWithAdminSupport for ReqwestHttpClientWithAdminSupport {
        async fn get_value_with_admin_override(
            &self,
            url: &str,
            admin_tenant: Option<&str>,
        ) -> Result<Value> {
            let mut request = self.client.get(url);

            // Add admin override headers if available
            let mut header_added = false;
            if let (Some(admin_key), Some(tenant)) = (&self.admin_key, admin_tenant) {
                let mut header_map = HeaderMap::new();
                
                // Add admin key header
                let admin_key_header = HeaderValue::from_str(admin_key)
                    .map_err(|e| crate::errors::Error::Network(format!("Invalid admin key: {}", e)))?;
                header_map.insert("x-admin-key", admin_key_header);
                
                // Add tenant override header
                let tenant_header = HeaderValue::from_str(tenant)
                    .map_err(|e| crate::errors::Error::Network(format!("Invalid tenant: {}", e)))?;
                header_map.insert("x-admin-tenant", tenant_header);
                
                request = request.headers(header_map);
                header_added = true;
            }

            tracing::info!(
                target: "xjp_oidc::http",
                "HTTP 请求: {} (admin_override: {})",
                url,
                header_added
            );

            let response = request
                .send()
                .await
                .map_err(|e| crate::errors::Error::Network(format!("Request failed: {}", e)))?;

            let status = response.status();
            let duration = std::time::Instant::now();

            if !status.is_success() {
                let error_body = response
                    .text()
                    .await
                    .unwrap_or_else(|_| "Failed to read error response".to_string());

                tracing::error!(
                    target: "xjp_oidc::http",
                    "HTTP 请求失败 url={} status={} error={}",
                    url,
                    status,
                    error_body
                );

                return Err(crate::errors::Error::Network(format!(
                    "HTTP {} - {}",
                    status, error_body
                )));
            }

            let value = response
                .json::<Value>()
                .await
                .map_err(|e| crate::errors::Error::Network(format!("Failed to parse JSON: {}", e)))?;

            tracing::info!(
                target: "xjp_oidc::http",
                "HTTP 请求完成 url={} method=\"GET\" duration_ms={} status={}",
                url,
                duration.elapsed().as_millis(),
                status.as_u16()
            );

            Ok(value)
        }
    }

    // Also implement the standard HttpClient trait
    #[async_trait]
    impl HttpClient for ReqwestHttpClientWithAdminSupport {
        async fn get_value(&self, url: &str) -> std::result::Result<Value, HttpClientError> {
            let response = self
                .client
                .get(url)
                .send()
                .await
                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;

            let status = response.status();
            if !status.is_success() {
                let error_body = response
                    .text()
                    .await
                    .unwrap_or_else(|_| "Failed to read error response".to_string());

                return Err(HttpClientError::InvalidStatus {
                    status: status.as_u16(),
                    message: error_body,
                });
            }

            response
                .json::<Value>()
                .await
                .map_err(|e| HttpClientError::ParseError(e.to_string()))
        }

        async fn post_form_value(
            &self,
            url: &str,
            form: &[(String, String)],
            auth_header: Option<(&str, &str)>,
        ) -> std::result::Result<Value, HttpClientError> {
            let mut request = self.client.post(url).form(form);

            if let Some((key, value)) = auth_header {
                let header_value = HeaderValue::from_str(value)
                    .map_err(|e| HttpClientError::RequestFailed(format!("Invalid header value: {}", e)))?;
                let header_name = HeaderName::from_bytes(key.as_bytes())
                    .map_err(|e| HttpClientError::RequestFailed(format!("Invalid header name: {}", e)))?;
                request = request.header(header_name, header_value);
            }

            let response = request
                .send()
                .await
                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;

            let status = response.status();
            if !status.is_success() {
                let error_body = response
                    .text()
                    .await
                    .unwrap_or_else(|_| "Failed to read error response".to_string());

                return Err(HttpClientError::InvalidStatus {
                    status: status.as_u16(),
                    message: error_body,
                });
            }

            response
                .json::<Value>()
                .await
                .map_err(|e| HttpClientError::ParseError(e.to_string()))
        }

        async fn post_json_value(
            &self,
            url: &str,
            body: &Value,
            auth_header: Option<(&str, &str)>,
        ) -> std::result::Result<Value, HttpClientError> {
            let mut request = self.client.post(url).json(body);

            if let Some((key, value)) = auth_header {
                let header_value = HeaderValue::from_str(value)
                    .map_err(|e| HttpClientError::RequestFailed(format!("Invalid header value: {}", e)))?;
                let header_name = HeaderName::from_bytes(key.as_bytes())
                    .map_err(|e| HttpClientError::RequestFailed(format!("Invalid header name: {}", e)))?;
                request = request.header(header_name, header_value);
            }

            let response = request
                .send()
                .await
                .map_err(|e| HttpClientError::RequestFailed(e.to_string()))?;

            let status = response.status();
            if !status.is_success() {
                let error_body = response
                    .text()
                    .await
                    .unwrap_or_else(|_| "Failed to read error response".to_string());

                return Err(HttpClientError::InvalidStatus {
                    status: status.as_u16(),
                    message: error_body,
                });
            }

            response
                .json::<Value>()
                .await
                .map_err(|e| HttpClientError::ParseError(e.to_string()))
        }
    }
}