gitdataai/libs/rpc/admin/client.rs
ZhenYi 418f9a5d8b
Some checks are pending
CI / Rust Lint & Check (push) Waiting to run
CI / Rust Tests (push) Waiting to run
CI / Frontend Lint & Type Check (push) Waiting to run
CI / Frontend Build (push) Blocked by required conditions
feat(rpc): migrate admin from Redis Pub/Sub JSON-RPC to Tonic gRPC
- libs/rpc/proto/: admin.proto with 8 RPC methods
- libs/rpc/admin/: tonic server impl (SessionAdminService), client
  wrapper (AdminGrpcClient), types, generated/ tonic-prost build output
- libs/rpc/build.rs: tonic-prost-build two-step (proto -> message types
  + manual service defs)
- libs/rpc/lib.rs: module re-exports
- libs/session_manager/: session manager types used by admin service
2026-04-21 13:44:25 +08:00

169 lines
6.2 KiB
Rust

//! Tonic gRPC client wrapper for SessionAdmin service.
use session_manager::{OnlineStatus, SessionInfo, UserSession};
use tonic::transport::Channel;
use uuid::Uuid;
use super::generated::admin::{
ListWorkspaceSessionsRequest, ListUserSessionsRequest,
KickUserFromWorkspaceRequest, KickUserRequest,
GetUserStatusRequest, GetUserInfoRequest,
GetWorkspaceOnlineUsersRequest, IsUserOnlineRequest,
};
use super::generated::admin_session_admin::session_admin_client::SessionAdminClient;
use super::types::from_proto_status;
/// Auto-generated gRPC client type.
pub type GrpcClient = SessionAdminClient<Channel>;
/// Thin wrapper around the generated SessionAdminClient.
pub struct AdminGrpcClient {
inner: GrpcClient,
}
impl AdminGrpcClient {
/// Connect to the gRPC server at the given URI.
pub async fn connect(uri: tonic::codegen::http::Uri) -> anyhow::Result<Self> {
let inner = SessionAdminClient::connect(uri).await?;
Ok(Self { inner })
}
/// Wrap an existing channel.
pub fn new(inner: GrpcClient) -> Self {
Self { inner }
}
pub async fn list_workspace_sessions(
&mut self,
workspace_id: Uuid,
) -> anyhow::Result<Vec<UserSession>> {
let req = tonic::Request::new(ListWorkspaceSessionsRequest {
workspace_id: workspace_id.to_string(),
});
let res = self.inner.list_workspace_sessions(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().sessions
.into_iter()
.map(from_proto_session)
.collect())
}
pub async fn list_user_sessions(
&mut self,
user_id: Uuid,
) -> anyhow::Result<Vec<UserSession>> {
let req = tonic::Request::new(ListUserSessionsRequest {
user_id: user_id.to_string(),
});
let res = self.inner.list_user_sessions(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().sessions
.into_iter()
.map(from_proto_session)
.collect())
}
pub async fn kick_user_from_workspace(
&mut self,
user_id: Uuid,
workspace_id: Uuid,
) -> anyhow::Result<usize> {
let req = tonic::Request::new(KickUserFromWorkspaceRequest {
user_id: user_id.to_string(),
workspace_id: workspace_id.to_string(),
});
let res = self.inner.kick_user_from_workspace(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().kicked_count as usize)
}
pub async fn kick_user(&mut self, user_id: Uuid) -> anyhow::Result<usize> {
let req = tonic::Request::new(KickUserRequest {
user_id: user_id.to_string(),
});
let res = self.inner.kick_user(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().kicked_count as usize)
}
pub async fn get_user_status(&mut self, user_id: Uuid) -> anyhow::Result<OnlineStatus> {
let req = tonic::Request::new(GetUserStatusRequest {
user_id: user_id.to_string(),
});
let res = self.inner.get_user_status(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
let status = super::generated::admin::OnlineStatus::try_from(res.get_ref().status)
.unwrap_or(super::generated::admin::OnlineStatus::Offline);
Ok(from_proto_status(status))
}
pub async fn get_user_info(
&mut self,
user_id: Uuid,
) -> anyhow::Result<Option<SessionInfo>> {
let req = tonic::Request::new(GetUserInfoRequest {
user_id: user_id.to_string(),
});
let res = self.inner.get_user_info(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().info.map(|i| from_proto_info(&i)))
}
pub async fn get_workspace_online_users(
&mut self,
workspace_id: Uuid,
) -> anyhow::Result<Vec<Uuid>> {
let req = tonic::Request::new(GetWorkspaceOnlineUsersRequest {
workspace_id: workspace_id.to_string(),
});
let res = self.inner.get_workspace_online_users(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
res.into_inner().user_ids
.into_iter()
.map(|s| Uuid::parse_str(&s))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| anyhow::anyhow!("invalid UUID in response: {}", e))
}
pub async fn is_user_online(&mut self, user_id: Uuid) -> anyhow::Result<bool> {
let req = tonic::Request::new(IsUserOnlineRequest {
user_id: user_id.to_string(),
});
let res = self.inner.is_user_online(req).await
.map_err(|e| anyhow::anyhow!("gRPC error: {}", e))?;
Ok(res.into_inner().online)
}
}
// ---------------------------------------------------------------------------
// Proto → session_manager type conversions
// ---------------------------------------------------------------------------
fn from_proto_session(s: super::generated::admin::UserSession) -> UserSession {
UserSession {
session_id: Uuid::parse_str(&s.session_id).unwrap_or_default(),
user_id: Uuid::parse_str(&s.user_id).unwrap_or_default(),
workspace_id: Uuid::parse_str(&s.workspace_id).unwrap_or_default(),
ip_address: s.ip_address,
user_agent: s.user_agent,
connected_at: s.connected_at
.and_then(|ts| chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32))
.unwrap_or_else(chrono::Utc::now),
last_heartbeat: s.last_heartbeat
.and_then(|ts| chrono::DateTime::from_timestamp(ts.seconds, ts.nanos as u32))
.unwrap_or_else(chrono::Utc::now),
}
}
fn from_proto_info(info: &super::generated::admin::SessionInfo) -> SessionInfo {
SessionInfo {
user_id: Uuid::parse_str(&info.user_id).unwrap_or_default(),
session_count: info.session_count as usize,
workspaces: info.workspaces
.iter()
.filter_map(|w| Uuid::parse_str(w).ok())
.collect(),
latest_session: info.latest_session.as_ref().map(|s| from_proto_session(s.clone())),
}
}