//! Aggregate gRPC client holder for all appks core services. //! //! A single TCP `Channel` is shared across all four service clients //! to avoid redundant connections. use std::fs; use std::time::Duration; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Endpoint, Identity}; use crate::pb::core::token_service_client::TokenServiceClient; use crate::pb::im::{ channel_service_client::ChannelServiceClient, member_service_client::MemberServiceClient, permission_service_client::PermissionServiceClient, }; use crate::{ImksError, ImksResult}; use super::config::RpcConfig; /// Holds gRPC clients for all appks core services consumed by imks. /// /// Cheaply cloneable — each inner client wraps a shared `Arc`. #[derive(Clone)] pub struct AppksClients { /// JWT token lifecycle: issue, refresh, revoke, verify, signing keys. pub token: TokenServiceClient, /// Channel and category CRUD + statistics. pub channel: ChannelServiceClient, /// Channel member invite / kick / join / leave. pub member: MemberServiceClient, /// Permission checks and overwrite rules. pub permission: PermissionServiceClient, } impl AppksClients { /// Connect to all appks services using a shared gRPC channel. pub async fn connect(config: &RpcConfig) -> ImksResult { let mut endpoint = Endpoint::from_shared(config.appks_addr.clone()) .map_err(|e| ImksError::Internal(format!("Invalid gRPC endpoint: {e}")))? .connect_timeout(Duration::from_secs(config.connect_timeout_secs)); if config.appks_addr.starts_with("https://") || config.tls_ca_cert_path.is_some() || config.tls_client_cert_path.is_some() || config.tls_client_key_path.is_some() { endpoint = endpoint.tls_config(build_tls_config(config)?)?; } let channel = endpoint .connect() .await .map_err(|e| ImksError::Internal(format!("gRPC connect failed: {e}")))?; tracing::info!(addr = %config.appks_addr, "Connected to appks gRPC services"); Ok(Self { token: TokenServiceClient::new(channel.clone()), channel: ChannelServiceClient::new(channel.clone()), member: MemberServiceClient::new(channel.clone()), permission: PermissionServiceClient::new(channel), }) } /// Build from pre-connected clients (useful for tests with mock servers). pub fn new( token: TokenServiceClient, channel: ChannelServiceClient, member: MemberServiceClient, permission: PermissionServiceClient, ) -> Self { Self { token, channel, member, permission, } } } fn build_tls_config(config: &RpcConfig) -> ImksResult { let mut tls = ClientTlsConfig::new(); if let Some(domain) = &config.tls_domain_name { tls = tls.domain_name(domain); } if let Some(path) = &config.tls_ca_cert_path { let pem = fs::read(path)?; tls = tls.ca_certificate(Certificate::from_pem(pem)); } match (&config.tls_client_cert_path, &config.tls_client_key_path) { (Some(cert_path), Some(key_path)) => { let cert = fs::read(cert_path)?; let key = fs::read(key_path)?; tls = tls.identity(Identity::from_pem(cert, key)); } (None, None) => {} _ => { return Err(ImksError::InvalidInput( "Both APPKS_GRPC_TLS_CLIENT_CERT and APPKS_GRPC_TLS_CLIENT_KEY are required for mTLS".into(), )); } } Ok(tls) }