//! Client gRPC pour communiquer avec veza-backend-api use crate::error::{ChatError, Result}; use serde::{Deserialize, Serialize}; use std::time::Duration; use tokio::time::timeout; use tracing::{info, warn, error}; /// Configuration du client gRPC #[derive(Debug, Clone)] pub struct GrpcClientConfig { pub backend_api_url: String, pub timeout_seconds: u64, pub max_retries: u32, } impl Default for GrpcClientConfig { fn default() -> Self { Self { backend_api_url: std::env::var("BACKEND_API_URL") .unwrap_or_else(|_| "http://localhost:8080".to_string()), timeout_seconds: 30, max_retries: 3, } } } /// Informations utilisateur depuis le backend #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UserInfo { pub id: i64, pub username: String, pub email: String, pub role: String, pub is_active: bool, } /// Informations de session utilisateur #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionInfo { pub user_id: i64, pub session_token: String, pub expires_at: String, pub is_active: bool, } /// Client gRPC pour communiquer avec le backend API pub struct BackendApiClient { config: GrpcClientConfig, http_client: reqwest::Client, } impl BackendApiClient { /// Crée un nouveau client pub fn new(config: GrpcClientConfig) -> Self { let http_client = reqwest::Client::builder() .timeout(Duration::from_secs(config.timeout_seconds)) .build() .unwrap_or_default(); Self { config, http_client, } } /// Crée un client avec la configuration par défaut pub fn with_default_config() -> Self { Self::new(GrpcClientConfig::default()) } /// Vérifie la validité d'un token JWT pub async fn verify_jwt_token(&self, token: &str) -> Result { info!("🔍 Vérification du token JWT avec le backend API"); let url = format!("{}/api/v1/auth/profile", self.config.backend_api_url); let response = timeout( Duration::from_secs(self.config.timeout_seconds), self.http_client .get(&url) .header("Authorization", format!("Bearer {}", token)) .send() ).await .map_err(|_| ChatError::configuration_error("Timeout lors de la vérification du token"))? .map_err(|e| ChatError::configuration_error(&format!("Erreur HTTP: {}", e)))?; if response.status().is_success() { let response_body: serde_json::Value = response.json().await .map_err(|e| ChatError::configuration_error(&format!("Erreur parsing JSON: {}", e)))?; // Parser la réponse du backend if let Some(data) = response_body.get("data") { if let Some(user_data) = data.get("user") { let user_info = UserInfo { id: user_data.get("id").and_then(|v| v.as_i64()).unwrap_or(0), username: user_data.get("username").and_then(|v| v.as_str()).unwrap_or("unknown").to_string(), email: user_data.get("email").and_then(|v| v.as_str()).unwrap_or("").to_string(), role: user_data.get("role").and_then(|v| v.as_str()).unwrap_or("user").to_string(), is_active: user_data.get("is_active").and_then(|v| v.as_bool()).unwrap_or(false), }; info!("✅ Token valide pour l'utilisateur: {}", user_info.username); return Ok(user_info); } } error!("❌ Réponse invalide du backend API"); Err(ChatError::configuration_error("Réponse invalide du backend API")) } else if response.status() == 401 { warn!("⚠️ Token JWT invalide ou expiré"); Err(ChatError::configuration_error("Token JWT invalide ou expiré")) } else { error!("❌ Erreur backend API: {}", response.status()); Err(ChatError::configuration_error(&format!("Erreur backend API: {}", response.status()))) } } /// Synchronise les informations utilisateur pub async fn sync_user_info(&self, user_id: i64) -> Result { info!("🔄 Synchronisation des informations utilisateur ID: {}", user_id); let url = format!("{}/api/v1/users/{}", self.config.backend_api_url, user_id); let response = timeout( Duration::from_secs(self.config.timeout_seconds), self.http_client.get(&url).send() ).await .map_err(|_| ChatError::configuration_error("Timeout lors de la synchronisation utilisateur"))? .map_err(|e| ChatError::configuration_error(&format!("Erreur HTTP: {}", e)))?; if response.status().is_success() { let user_info: UserInfo = response.json().await .map_err(|e| ChatError::configuration_error(&format!("Erreur parsing user info: {}", e)))?; info!("✅ Informations utilisateur synchronisées: {}", user_info.username); Ok(user_info) } else { error!("❌ Impossible de récupérer les informations utilisateur: {}", response.status()); Err(ChatError::configuration_error(&format!("Erreur récupération utilisateur: {}", response.status()))) } } /// Notifie le backend d'un nouveau message de chat pub async fn notify_new_message(&self, message_id: i32, room_id: &str, sender_id: i64) -> Result<()> { info!("📢 Notification nouveau message au backend: message={}, room={}, sender={}", message_id, room_id, sender_id); let url = format!("{}/api/v1/chat/message-notifications", self.config.backend_api_url); let payload = serde_json::json!({ "message_id": message_id, "room_id": room_id, "sender_id": sender_id, "timestamp": chrono::Utc::now() }); let response = timeout( Duration::from_secs(self.config.timeout_seconds), self.http_client .post(&url) .json(&payload) .send() ).await .map_err(|_| ChatError::configuration_error("Timeout lors de la notification"))? .map_err(|e| ChatError::configuration_error(&format!("Erreur HTTP notification: {}", e)))?; if response.status().is_success() { info!("✅ Notification envoyée avec succès"); Ok(()) } else { warn!("⚠️ Échec notification (non critique): {}", response.status()); // Les notifications sont non-critiques, ne pas faire échouer l'opération Ok(()) } } /// Vérifie la santé du backend API pub async fn health_check(&self) -> Result { let url = format!("{}/health", self.config.backend_api_url); match timeout( Duration::from_secs(5), // Timeout court pour les health checks self.http_client.get(&url).send() ).await { Ok(Ok(response)) => Ok(response.status().is_success()), _ => Ok(false), } } /// Met à jour les statistiques d'activité utilisateur pub async fn update_user_activity(&self, user_id: i64, activity_type: &str) -> Result<()> { let url = format!("{}/api/v1/users/{}/activity", self.config.backend_api_url, user_id); let payload = serde_json::json!({ "activity_type": activity_type, "timestamp": chrono::Utc::now() }); let response = timeout( Duration::from_secs(self.config.timeout_seconds), self.http_client .post(&url) .json(&payload) .send() ).await .map_err(|_| ChatError::configuration_error("Timeout lors de la mise à jour d'activité"))? .map_err(|e| ChatError::configuration_error(&format!("Erreur HTTP activité: {}", e)))?; if response.status().is_success() { info!("✅ Activité utilisateur mise à jour: {} - {}", user_id, activity_type); Ok(()) } else { warn!("⚠️ Échec mise à jour activité (non critique): {}", response.status()); // Les mises à jour d'activité sont non-critiques Ok(()) } } }