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
This commit is contained in:
parent
81e6ee3d48
commit
418f9a5d8b
@ -15,6 +15,26 @@ documentation.workspace = true
|
||||
path = "lib.rs"
|
||||
name = "rpc"
|
||||
[dependencies]
|
||||
# gRPC / Prost
|
||||
tonic = { workspace = true }
|
||||
prost = { workspace = true }
|
||||
prost-types = { workspace = true }
|
||||
|
||||
# Internal
|
||||
session_manager = { workspace = true }
|
||||
|
||||
# Logging
|
||||
slog = { workspace = true }
|
||||
|
||||
# Utilities
|
||||
anyhow = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tonic-prost-build = "0.14.5"
|
||||
prost-types = { workspace = true }
|
||||
|
||||
168
libs/rpc/admin/client.rs
Normal file
168
libs/rpc/admin/client.rs
Normal file
@ -0,0 +1,168 @@
|
||||
//! 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())),
|
||||
}
|
||||
}
|
||||
906
libs/rpc/admin/generated/admin.SessionAdmin.rs
Normal file
906
libs/rpc/admin/generated/admin.SessionAdmin.rs
Normal file
@ -0,0 +1,906 @@
|
||||
/// Generated client implementations.
|
||||
pub mod session_admin_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
use tonic::codegen::http::Uri;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionAdminClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl SessionAdminClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> SessionAdminClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> SessionAdminClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
SessionAdminClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
pub async fn list_workspace_sessions(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::ListWorkspaceSessionsRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<
|
||||
crate::admin::generated::admin::ListWorkspaceSessionsResponse,
|
||||
>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/ListWorkspaceSessions",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "ListWorkspaceSessions"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn list_user_sessions(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::ListUserSessionsRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::ListUserSessionsResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/ListUserSessions",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "ListUserSessions"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn kick_user_from_workspace(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::KickUserFromWorkspaceRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<
|
||||
crate::admin::generated::admin::KickUserFromWorkspaceResponse,
|
||||
>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/KickUserFromWorkspace",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "KickUserFromWorkspace"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn kick_user(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::KickUserRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::KickUserResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/KickUser",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "KickUser"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_user_status(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::GetUserStatusRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::GetUserStatusResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/GetUserStatus",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "GetUserStatus"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_user_info(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::GetUserInfoRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::GetUserInfoResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/GetUserInfo",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "GetUserInfo"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_workspace_online_users(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::GetWorkspaceOnlineUsersRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<
|
||||
crate::admin::generated::admin::GetWorkspaceOnlineUsersResponse,
|
||||
>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/GetWorkspaceOnlineUsers",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("admin.SessionAdmin", "GetWorkspaceOnlineUsers"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn is_user_online(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<
|
||||
crate::admin::generated::admin::IsUserOnlineRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::IsUserOnlineResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/IsUserOnline",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "IsUserOnline"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod session_admin_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with SessionAdminServer.
|
||||
#[async_trait]
|
||||
pub trait SessionAdmin: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn list_workspace_sessions(
|
||||
&self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::ListWorkspaceSessionsRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<
|
||||
crate::admin::generated::admin::ListWorkspaceSessionsResponse,
|
||||
>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn list_user_sessions(
|
||||
&self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::ListUserSessionsRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::ListUserSessionsResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn kick_user_from_workspace(
|
||||
&self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::KickUserFromWorkspaceRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<
|
||||
crate::admin::generated::admin::KickUserFromWorkspaceResponse,
|
||||
>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn kick_user(
|
||||
&self,
|
||||
request: tonic::Request<crate::admin::generated::admin::KickUserRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::KickUserResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_user_status(
|
||||
&self,
|
||||
request: tonic::Request<crate::admin::generated::admin::GetUserStatusRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::GetUserStatusResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_user_info(
|
||||
&self,
|
||||
request: tonic::Request<crate::admin::generated::admin::GetUserInfoRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::GetUserInfoResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_workspace_online_users(
|
||||
&self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::GetWorkspaceOnlineUsersRequest,
|
||||
>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<
|
||||
crate::admin::generated::admin::GetWorkspaceOnlineUsersResponse,
|
||||
>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn is_user_online(
|
||||
&self,
|
||||
request: tonic::Request<crate::admin::generated::admin::IsUserOnlineRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<crate::admin::generated::admin::IsUserOnlineResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct SessionAdminServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> SessionAdminServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: Default::default(),
|
||||
send_compression_encodings: Default::default(),
|
||||
max_decoding_message_size: None,
|
||||
max_encoding_message_size: None,
|
||||
}
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> InterceptedService<Self, F>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
{
|
||||
InterceptedService::new(Self::new(inner), interceptor)
|
||||
}
|
||||
/// Enable decompressing requests with the given encoding.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.accept_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Compress responses with the given encoding, if the client supports it.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.send_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_decoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_encoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for SessionAdminServer<T>
|
||||
where
|
||||
T: SessionAdmin,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/admin.SessionAdmin/ListWorkspaceSessions" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ListWorkspaceSessionsSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::ListWorkspaceSessionsRequest,
|
||||
> for ListWorkspaceSessionsSvc<T> {
|
||||
type Response = crate::admin::generated::admin::ListWorkspaceSessionsResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::ListWorkspaceSessionsRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::list_workspace_sessions(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = ListWorkspaceSessionsSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/ListUserSessions" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ListUserSessionsSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::ListUserSessionsRequest,
|
||||
> for ListUserSessionsSvc<T> {
|
||||
type Response = crate::admin::generated::admin::ListUserSessionsResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::ListUserSessionsRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::list_user_sessions(&inner, request)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = ListUserSessionsSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/KickUserFromWorkspace" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct KickUserFromWorkspaceSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::KickUserFromWorkspaceRequest,
|
||||
> for KickUserFromWorkspaceSvc<T> {
|
||||
type Response = crate::admin::generated::admin::KickUserFromWorkspaceResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::KickUserFromWorkspaceRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::kick_user_from_workspace(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = KickUserFromWorkspaceSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/KickUser" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct KickUserSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::KickUserRequest,
|
||||
> for KickUserSvc<T> {
|
||||
type Response = crate::admin::generated::admin::KickUserResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::KickUserRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::kick_user(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = KickUserSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/GetUserStatus" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUserStatusSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::GetUserStatusRequest,
|
||||
> for GetUserStatusSvc<T> {
|
||||
type Response = crate::admin::generated::admin::GetUserStatusResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::GetUserStatusRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::get_user_status(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetUserStatusSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/GetUserInfo" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUserInfoSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::GetUserInfoRequest,
|
||||
> for GetUserInfoSvc<T> {
|
||||
type Response = crate::admin::generated::admin::GetUserInfoResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::GetUserInfoRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::get_user_info(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetUserInfoSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/GetWorkspaceOnlineUsers" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetWorkspaceOnlineUsersSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::GetWorkspaceOnlineUsersRequest,
|
||||
> for GetWorkspaceOnlineUsersSvc<T> {
|
||||
type Response = crate::admin::generated::admin::GetWorkspaceOnlineUsersResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::GetWorkspaceOnlineUsersRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::get_workspace_online_users(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetWorkspaceOnlineUsersSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/IsUserOnline" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct IsUserOnlineSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<
|
||||
crate::admin::generated::admin::IsUserOnlineRequest,
|
||||
> for IsUserOnlineSvc<T> {
|
||||
type Response = crate::admin::generated::admin::IsUserOnlineResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
crate::admin::generated::admin::IsUserOnlineRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::is_user_online(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = IsUserOnlineSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
_ => {
|
||||
Box::pin(async move {
|
||||
let mut response = http::Response::new(
|
||||
tonic::body::Body::default(),
|
||||
);
|
||||
let headers = response.headers_mut();
|
||||
headers
|
||||
.insert(
|
||||
tonic::Status::GRPC_STATUS,
|
||||
(tonic::Code::Unimplemented as i32).into(),
|
||||
);
|
||||
headers
|
||||
.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||
);
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for SessionAdminServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: self.accept_compression_encodings,
|
||||
send_compression_encodings: self.send_compression_encodings,
|
||||
max_decoding_message_size: self.max_decoding_message_size,
|
||||
max_encoding_message_size: self.max_encoding_message_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "admin.SessionAdmin";
|
||||
impl<T> tonic::server::NamedService for SessionAdminServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
991
libs/rpc/admin/generated/admin.rs
Normal file
991
libs/rpc/admin/generated/admin.rs
Normal file
@ -0,0 +1,991 @@
|
||||
// This file is @generated by prost-build.
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct UserSession {
|
||||
#[prost(string, tag = "1")]
|
||||
pub session_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "3")]
|
||||
pub workspace_id: ::prost::alloc::string::String,
|
||||
#[prost(string, optional, tag = "4")]
|
||||
pub ip_address: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(string, optional, tag = "5")]
|
||||
pub user_agent: ::core::option::Option<::prost::alloc::string::String>,
|
||||
#[prost(message, optional, tag = "6")]
|
||||
pub connected_at: ::core::option::Option<::prost_types::Timestamp>,
|
||||
#[prost(message, optional, tag = "7")]
|
||||
pub last_heartbeat: ::core::option::Option<::prost_types::Timestamp>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct SessionInfo {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub session_count: u32,
|
||||
#[prost(string, repeated, tag = "3")]
|
||||
pub workspaces: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
#[prost(message, optional, tag = "4")]
|
||||
pub latest_session: ::core::option::Option<UserSession>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ListWorkspaceSessionsRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub workspace_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ListWorkspaceSessionsResponse {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub sessions: ::prost::alloc::vec::Vec<UserSession>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct ListUserSessionsRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ListUserSessionsResponse {
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub sessions: ::prost::alloc::vec::Vec<UserSession>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct KickUserFromWorkspaceRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
#[prost(string, tag = "2")]
|
||||
pub workspace_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct KickUserFromWorkspaceResponse {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub kicked_count: u32,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct KickUserRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct KickUserResponse {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub kicked_count: u32,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetUserStatusRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetUserStatusResponse {
|
||||
#[prost(enumeration = "OnlineStatus", tag = "1")]
|
||||
pub status: i32,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetUserInfoRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetUserInfoResponse {
|
||||
#[prost(message, optional, tag = "1")]
|
||||
pub info: ::core::option::Option<SessionInfo>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetWorkspaceOnlineUsersRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub workspace_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct GetWorkspaceOnlineUsersResponse {
|
||||
#[prost(string, repeated, tag = "1")]
|
||||
pub user_ids: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
|
||||
}
|
||||
#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct IsUserOnlineRequest {
|
||||
#[prost(string, tag = "1")]
|
||||
pub user_id: ::prost::alloc::string::String,
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)]
|
||||
pub struct IsUserOnlineResponse {
|
||||
#[prost(bool, tag = "1")]
|
||||
pub online: bool,
|
||||
}
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
|
||||
#[repr(i32)]
|
||||
pub enum OnlineStatus {
|
||||
Unspecified = 0,
|
||||
Online = 1,
|
||||
Idle = 2,
|
||||
Offline = 3,
|
||||
}
|
||||
impl OnlineStatus {
|
||||
/// String value of the enum field names used in the ProtoBuf definition.
|
||||
///
|
||||
/// The values are not transformed in any way and thus are considered stable
|
||||
/// (if the ProtoBuf definition does not change) and safe for programmatic use.
|
||||
pub fn as_str_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Unspecified => "ONLINE_STATUS_UNSPECIFIED",
|
||||
Self::Online => "ONLINE_STATUS_ONLINE",
|
||||
Self::Idle => "ONLINE_STATUS_IDLE",
|
||||
Self::Offline => "ONLINE_STATUS_OFFLINE",
|
||||
}
|
||||
}
|
||||
/// Creates an enum from field names used in the ProtoBuf definition.
|
||||
pub fn from_str_name(value: &str) -> ::core::option::Option<Self> {
|
||||
match value {
|
||||
"ONLINE_STATUS_UNSPECIFIED" => Some(Self::Unspecified),
|
||||
"ONLINE_STATUS_ONLINE" => Some(Self::Online),
|
||||
"ONLINE_STATUS_IDLE" => Some(Self::Idle),
|
||||
"ONLINE_STATUS_OFFLINE" => Some(Self::Offline),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated client implementations.
|
||||
pub mod session_admin_client {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
use tonic::codegen::http::Uri;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionAdminClient<T> {
|
||||
inner: tonic::client::Grpc<T>,
|
||||
}
|
||||
impl SessionAdminClient<tonic::transport::Channel> {
|
||||
/// Attempt to create a new client by connecting to a given endpoint.
|
||||
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
|
||||
where
|
||||
D: TryInto<tonic::transport::Endpoint>,
|
||||
D::Error: Into<StdError>,
|
||||
{
|
||||
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
|
||||
Ok(Self::new(conn))
|
||||
}
|
||||
}
|
||||
impl<T> SessionAdminClient<T>
|
||||
where
|
||||
T: tonic::client::GrpcService<tonic::body::Body>,
|
||||
T::Error: Into<StdError>,
|
||||
T::ResponseBody: Body<Data = Bytes> + std::marker::Send + 'static,
|
||||
<T::ResponseBody as Body>::Error: Into<StdError> + std::marker::Send,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
let inner = tonic::client::Grpc::new(inner);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_origin(inner: T, origin: Uri) -> Self {
|
||||
let inner = tonic::client::Grpc::with_origin(inner, origin);
|
||||
Self { inner }
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> SessionAdminClient<InterceptedService<T, F>>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
T::ResponseBody: Default,
|
||||
T: tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
Response = http::Response<
|
||||
<T as tonic::client::GrpcService<tonic::body::Body>>::ResponseBody,
|
||||
>,
|
||||
>,
|
||||
<T as tonic::codegen::Service<
|
||||
http::Request<tonic::body::Body>,
|
||||
>>::Error: Into<StdError> + std::marker::Send + std::marker::Sync,
|
||||
{
|
||||
SessionAdminClient::new(InterceptedService::new(inner, interceptor))
|
||||
}
|
||||
/// Compress requests with the given encoding.
|
||||
///
|
||||
/// This requires the server to support it otherwise it might respond with an
|
||||
/// error.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.send_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Enable decompressing responses.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.inner = self.inner.accept_compressed(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_decoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.inner = self.inner.max_encoding_message_size(limit);
|
||||
self
|
||||
}
|
||||
pub async fn list_workspace_sessions(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ListWorkspaceSessionsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ListWorkspaceSessionsResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/ListWorkspaceSessions",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "ListWorkspaceSessions"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn list_user_sessions(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::ListUserSessionsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ListUserSessionsResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/ListUserSessions",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "ListUserSessions"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn kick_user_from_workspace(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::KickUserFromWorkspaceRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::KickUserFromWorkspaceResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/KickUserFromWorkspace",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "KickUserFromWorkspace"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn kick_user(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::KickUserRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::KickUserResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/KickUser",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "KickUser"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_user_status(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::GetUserStatusRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetUserStatusResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/GetUserStatus",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "GetUserStatus"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_user_info(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::GetUserInfoRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetUserInfoResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/GetUserInfo",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "GetUserInfo"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn get_workspace_online_users(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::GetWorkspaceOnlineUsersRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetWorkspaceOnlineUsersResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/GetWorkspaceOnlineUsers",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(
|
||||
GrpcMethod::new("admin.SessionAdmin", "GetWorkspaceOnlineUsers"),
|
||||
);
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
pub async fn is_user_online(
|
||||
&mut self,
|
||||
request: impl tonic::IntoRequest<super::IsUserOnlineRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::IsUserOnlineResponse>,
|
||||
tonic::Status,
|
||||
> {
|
||||
self.inner
|
||||
.ready()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tonic::Status::unknown(
|
||||
format!("Service was not ready: {}", e.into()),
|
||||
)
|
||||
})?;
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let path = http::uri::PathAndQuery::from_static(
|
||||
"/admin.SessionAdmin/IsUserOnline",
|
||||
);
|
||||
let mut req = request.into_request();
|
||||
req.extensions_mut()
|
||||
.insert(GrpcMethod::new("admin.SessionAdmin", "IsUserOnline"));
|
||||
self.inner.unary(req, path, codec).await
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated server implementations.
|
||||
pub mod session_admin_server {
|
||||
#![allow(
|
||||
unused_variables,
|
||||
dead_code,
|
||||
missing_docs,
|
||||
clippy::wildcard_imports,
|
||||
clippy::let_unit_value,
|
||||
)]
|
||||
use tonic::codegen::*;
|
||||
/// Generated trait containing gRPC methods that should be implemented for use with SessionAdminServer.
|
||||
#[async_trait]
|
||||
pub trait SessionAdmin: std::marker::Send + std::marker::Sync + 'static {
|
||||
async fn list_workspace_sessions(
|
||||
&self,
|
||||
request: tonic::Request<super::ListWorkspaceSessionsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ListWorkspaceSessionsResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn list_user_sessions(
|
||||
&self,
|
||||
request: tonic::Request<super::ListUserSessionsRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::ListUserSessionsResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn kick_user_from_workspace(
|
||||
&self,
|
||||
request: tonic::Request<super::KickUserFromWorkspaceRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::KickUserFromWorkspaceResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn kick_user(
|
||||
&self,
|
||||
request: tonic::Request<super::KickUserRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::KickUserResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_user_status(
|
||||
&self,
|
||||
request: tonic::Request<super::GetUserStatusRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetUserStatusResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_user_info(
|
||||
&self,
|
||||
request: tonic::Request<super::GetUserInfoRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetUserInfoResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn get_workspace_online_users(
|
||||
&self,
|
||||
request: tonic::Request<super::GetWorkspaceOnlineUsersRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::GetWorkspaceOnlineUsersResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
async fn is_user_online(
|
||||
&self,
|
||||
request: tonic::Request<super::IsUserOnlineRequest>,
|
||||
) -> std::result::Result<
|
||||
tonic::Response<super::IsUserOnlineResponse>,
|
||||
tonic::Status,
|
||||
>;
|
||||
}
|
||||
#[derive(Debug)]
|
||||
pub struct SessionAdminServer<T> {
|
||||
inner: Arc<T>,
|
||||
accept_compression_encodings: EnabledCompressionEncodings,
|
||||
send_compression_encodings: EnabledCompressionEncodings,
|
||||
max_decoding_message_size: Option<usize>,
|
||||
max_encoding_message_size: Option<usize>,
|
||||
}
|
||||
impl<T> SessionAdminServer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self::from_arc(Arc::new(inner))
|
||||
}
|
||||
pub fn from_arc(inner: Arc<T>) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: Default::default(),
|
||||
send_compression_encodings: Default::default(),
|
||||
max_decoding_message_size: None,
|
||||
max_encoding_message_size: None,
|
||||
}
|
||||
}
|
||||
pub fn with_interceptor<F>(
|
||||
inner: T,
|
||||
interceptor: F,
|
||||
) -> InterceptedService<Self, F>
|
||||
where
|
||||
F: tonic::service::Interceptor,
|
||||
{
|
||||
InterceptedService::new(Self::new(inner), interceptor)
|
||||
}
|
||||
/// Enable decompressing requests with the given encoding.
|
||||
#[must_use]
|
||||
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.accept_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Compress responses with the given encoding, if the client supports it.
|
||||
#[must_use]
|
||||
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
|
||||
self.send_compression_encodings.enable(encoding);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of a decoded message.
|
||||
///
|
||||
/// Default: `4MB`
|
||||
#[must_use]
|
||||
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_decoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
/// Limits the maximum size of an encoded message.
|
||||
///
|
||||
/// Default: `usize::MAX`
|
||||
#[must_use]
|
||||
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
|
||||
self.max_encoding_message_size = Some(limit);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl<T, B> tonic::codegen::Service<http::Request<B>> for SessionAdminServer<T>
|
||||
where
|
||||
T: SessionAdmin,
|
||||
B: Body + std::marker::Send + 'static,
|
||||
B::Error: Into<StdError> + std::marker::Send + 'static,
|
||||
{
|
||||
type Response = http::Response<tonic::body::Body>;
|
||||
type Error = std::convert::Infallible;
|
||||
type Future = BoxFuture<Self::Response, Self::Error>;
|
||||
fn poll_ready(
|
||||
&mut self,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<std::result::Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
fn call(&mut self, req: http::Request<B>) -> Self::Future {
|
||||
match req.uri().path() {
|
||||
"/admin.SessionAdmin/ListWorkspaceSessions" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ListWorkspaceSessionsSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::ListWorkspaceSessionsRequest>
|
||||
for ListWorkspaceSessionsSvc<T> {
|
||||
type Response = super::ListWorkspaceSessionsResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::ListWorkspaceSessionsRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::list_workspace_sessions(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = ListWorkspaceSessionsSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/ListUserSessions" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct ListUserSessionsSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::ListUserSessionsRequest>
|
||||
for ListUserSessionsSvc<T> {
|
||||
type Response = super::ListUserSessionsResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::ListUserSessionsRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::list_user_sessions(&inner, request)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = ListUserSessionsSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/KickUserFromWorkspace" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct KickUserFromWorkspaceSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::KickUserFromWorkspaceRequest>
|
||||
for KickUserFromWorkspaceSvc<T> {
|
||||
type Response = super::KickUserFromWorkspaceResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::KickUserFromWorkspaceRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::kick_user_from_workspace(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = KickUserFromWorkspaceSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/KickUser" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct KickUserSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::KickUserRequest>
|
||||
for KickUserSvc<T> {
|
||||
type Response = super::KickUserResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::KickUserRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::kick_user(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = KickUserSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/GetUserStatus" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUserStatusSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::GetUserStatusRequest>
|
||||
for GetUserStatusSvc<T> {
|
||||
type Response = super::GetUserStatusResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::GetUserStatusRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::get_user_status(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetUserStatusSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/GetUserInfo" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetUserInfoSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::GetUserInfoRequest>
|
||||
for GetUserInfoSvc<T> {
|
||||
type Response = super::GetUserInfoResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::GetUserInfoRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::get_user_info(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetUserInfoSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/GetWorkspaceOnlineUsers" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct GetWorkspaceOnlineUsersSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::GetWorkspaceOnlineUsersRequest>
|
||||
for GetWorkspaceOnlineUsersSvc<T> {
|
||||
type Response = super::GetWorkspaceOnlineUsersResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<
|
||||
super::GetWorkspaceOnlineUsersRequest,
|
||||
>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::get_workspace_online_users(
|
||||
&inner,
|
||||
request,
|
||||
)
|
||||
.await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = GetWorkspaceOnlineUsersSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
"/admin.SessionAdmin/IsUserOnline" => {
|
||||
#[allow(non_camel_case_types)]
|
||||
struct IsUserOnlineSvc<T: SessionAdmin>(pub Arc<T>);
|
||||
impl<
|
||||
T: SessionAdmin,
|
||||
> tonic::server::UnaryService<super::IsUserOnlineRequest>
|
||||
for IsUserOnlineSvc<T> {
|
||||
type Response = super::IsUserOnlineResponse;
|
||||
type Future = BoxFuture<
|
||||
tonic::Response<Self::Response>,
|
||||
tonic::Status,
|
||||
>;
|
||||
fn call(
|
||||
&mut self,
|
||||
request: tonic::Request<super::IsUserOnlineRequest>,
|
||||
) -> Self::Future {
|
||||
let inner = Arc::clone(&self.0);
|
||||
let fut = async move {
|
||||
<T as SessionAdmin>::is_user_online(&inner, request).await
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
}
|
||||
let accept_compression_encodings = self.accept_compression_encodings;
|
||||
let send_compression_encodings = self.send_compression_encodings;
|
||||
let max_decoding_message_size = self.max_decoding_message_size;
|
||||
let max_encoding_message_size = self.max_encoding_message_size;
|
||||
let inner = self.inner.clone();
|
||||
let fut = async move {
|
||||
let method = IsUserOnlineSvc(inner);
|
||||
let codec = tonic_prost::ProstCodec::default();
|
||||
let mut grpc = tonic::server::Grpc::new(codec)
|
||||
.apply_compression_config(
|
||||
accept_compression_encodings,
|
||||
send_compression_encodings,
|
||||
)
|
||||
.apply_max_message_size_config(
|
||||
max_decoding_message_size,
|
||||
max_encoding_message_size,
|
||||
);
|
||||
let res = grpc.unary(method, req).await;
|
||||
Ok(res)
|
||||
};
|
||||
Box::pin(fut)
|
||||
}
|
||||
_ => {
|
||||
Box::pin(async move {
|
||||
let mut response = http::Response::new(
|
||||
tonic::body::Body::default(),
|
||||
);
|
||||
let headers = response.headers_mut();
|
||||
headers
|
||||
.insert(
|
||||
tonic::Status::GRPC_STATUS,
|
||||
(tonic::Code::Unimplemented as i32).into(),
|
||||
);
|
||||
headers
|
||||
.insert(
|
||||
http::header::CONTENT_TYPE,
|
||||
tonic::metadata::GRPC_CONTENT_TYPE,
|
||||
);
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<T> Clone for SessionAdminServer<T> {
|
||||
fn clone(&self) -> Self {
|
||||
let inner = self.inner.clone();
|
||||
Self {
|
||||
inner,
|
||||
accept_compression_encodings: self.accept_compression_encodings,
|
||||
send_compression_encodings: self.send_compression_encodings,
|
||||
max_decoding_message_size: self.max_decoding_message_size,
|
||||
max_encoding_message_size: self.max_encoding_message_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Generated gRPC service name
|
||||
pub const SERVICE_NAME: &str = "admin.SessionAdmin";
|
||||
impl<T> tonic::server::NamedService for SessionAdminServer<T> {
|
||||
const NAME: &'static str = SERVICE_NAME;
|
||||
}
|
||||
}
|
||||
7
libs/rpc/admin/generated/mod.rs
Normal file
7
libs/rpc/admin/generated/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
// Generated message types from proto/admin.proto.
|
||||
|
||||
pub mod admin;
|
||||
|
||||
// Generated tonic service client/server from manual definitions.
|
||||
#[path = "admin.SessionAdmin.rs"]
|
||||
pub mod admin_session_admin;
|
||||
215
libs/rpc/admin/server.rs
Normal file
215
libs/rpc/admin/server.rs
Normal file
@ -0,0 +1,215 @@
|
||||
//! Tonic gRPC server implementation for SessionAdmin service.
|
||||
|
||||
use session_manager::SessionManager;
|
||||
use slog::Logger;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::sync::broadcast;
|
||||
use tonic::{transport::Server, Request, Response, Status};
|
||||
|
||||
use super::generated::admin::{
|
||||
GetUserInfoRequest, GetUserInfoResponse, GetUserStatusRequest, GetUserStatusResponse,
|
||||
GetWorkspaceOnlineUsersRequest, GetWorkspaceOnlineUsersResponse, IsUserOnlineRequest,
|
||||
IsUserOnlineResponse, KickUserFromWorkspaceRequest, KickUserFromWorkspaceResponse,
|
||||
KickUserRequest, KickUserResponse, ListUserSessionsRequest, ListUserSessionsResponse,
|
||||
ListWorkspaceSessionsRequest, ListWorkspaceSessionsResponse,
|
||||
};
|
||||
use super::generated::admin_session_admin::session_admin_server::{
|
||||
SessionAdmin, SessionAdminServer,
|
||||
};
|
||||
use super::types::{parse_uuid, to_proto_info, to_proto_session, to_proto_status};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionAdminService {
|
||||
session_manager: SessionManager,
|
||||
}
|
||||
|
||||
impl SessionAdminService {
|
||||
pub fn new(session_manager: SessionManager) -> Self {
|
||||
Self { session_manager }
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl SessionAdmin for SessionAdminService {
|
||||
async fn list_workspace_sessions(
|
||||
&self,
|
||||
req: Request<ListWorkspaceSessionsRequest>,
|
||||
) -> Result<Response<ListWorkspaceSessionsResponse>, Status> {
|
||||
let workspace_id = parse_uuid(&req.get_ref().workspace_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid workspace_id"))?;
|
||||
|
||||
let sessions = self
|
||||
.session_manager
|
||||
.get_workspace_sessions(&workspace_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
let sessions = sessions.iter().map(to_proto_session).collect();
|
||||
Ok(Response::new(ListWorkspaceSessionsResponse { sessions }))
|
||||
}
|
||||
|
||||
async fn list_user_sessions(
|
||||
&self,
|
||||
req: Request<ListUserSessionsRequest>,
|
||||
) -> Result<Response<ListUserSessionsResponse>, Status> {
|
||||
let user_id = parse_uuid(&req.get_ref().user_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid user_id"))?;
|
||||
|
||||
let sessions = self
|
||||
.session_manager
|
||||
.get_user_sessions(&user_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
let sessions = sessions.iter().map(to_proto_session).collect();
|
||||
Ok(Response::new(ListUserSessionsResponse { sessions }))
|
||||
}
|
||||
|
||||
async fn kick_user_from_workspace(
|
||||
&self,
|
||||
req: Request<KickUserFromWorkspaceRequest>,
|
||||
) -> Result<Response<KickUserFromWorkspaceResponse>, Status> {
|
||||
let r = req.get_ref();
|
||||
let user_id =
|
||||
parse_uuid(&r.user_id).ok_or_else(|| Status::invalid_argument("invalid user_id"))?;
|
||||
let workspace_id = parse_uuid(&r.workspace_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid workspace_id"))?;
|
||||
|
||||
let kicked_count = self
|
||||
.session_manager
|
||||
.kick_user_from_workspace(&user_id, &workspace_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
Ok(Response::new(KickUserFromWorkspaceResponse {
|
||||
kicked_count: kicked_count as u32,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn kick_user(
|
||||
&self,
|
||||
req: Request<KickUserRequest>,
|
||||
) -> Result<Response<KickUserResponse>, Status> {
|
||||
let user_id = parse_uuid(&req.get_ref().user_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid user_id"))?;
|
||||
|
||||
let kicked_count = self
|
||||
.session_manager
|
||||
.kick_user(&user_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
Ok(Response::new(KickUserResponse {
|
||||
kicked_count: kicked_count as u32,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_user_status(
|
||||
&self,
|
||||
req: Request<GetUserStatusRequest>,
|
||||
) -> Result<Response<GetUserStatusResponse>, Status> {
|
||||
let user_id = parse_uuid(&req.get_ref().user_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid user_id"))?;
|
||||
|
||||
let status = self
|
||||
.session_manager
|
||||
.get_user_status(&user_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
Ok(Response::new(GetUserStatusResponse {
|
||||
status: to_proto_status(status) as i32,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_user_info(
|
||||
&self,
|
||||
req: Request<GetUserInfoRequest>,
|
||||
) -> Result<Response<GetUserInfoResponse>, Status> {
|
||||
let user_id = parse_uuid(&req.get_ref().user_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid user_id"))?;
|
||||
|
||||
let info = self
|
||||
.session_manager
|
||||
.get_user_info(&user_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
Ok(Response::new(GetUserInfoResponse {
|
||||
info: info.as_ref().map(to_proto_info),
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_workspace_online_users(
|
||||
&self,
|
||||
req: Request<GetWorkspaceOnlineUsersRequest>,
|
||||
) -> Result<Response<GetWorkspaceOnlineUsersResponse>, Status> {
|
||||
let workspace_id = parse_uuid(&req.get_ref().workspace_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid workspace_id"))?;
|
||||
|
||||
let user_ids = self
|
||||
.session_manager
|
||||
.get_workspace_online_users(&workspace_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
let user_ids = user_ids.iter().map(|u| u.to_string()).collect();
|
||||
Ok(Response::new(GetWorkspaceOnlineUsersResponse { user_ids }))
|
||||
}
|
||||
|
||||
async fn is_user_online(
|
||||
&self,
|
||||
req: Request<IsUserOnlineRequest>,
|
||||
) -> Result<Response<IsUserOnlineResponse>, Status> {
|
||||
let user_id = parse_uuid(&req.get_ref().user_id)
|
||||
.ok_or_else(|| Status::invalid_argument("invalid user_id"))?;
|
||||
|
||||
let online = self
|
||||
.session_manager
|
||||
.is_user_online(&user_id)
|
||||
.await
|
||||
.map_err(|e| Status::internal(e.to_string()))?;
|
||||
|
||||
Ok(Response::new(IsUserOnlineResponse { online }))
|
||||
}
|
||||
}
|
||||
|
||||
/// Default gRPC admin port.
|
||||
pub const DEFAULT_GRPC_PORT: u16 = 9090;
|
||||
|
||||
/// Start the Tonic gRPC server on the given address.
|
||||
pub async fn serve(
|
||||
addr: SocketAddr,
|
||||
session_manager: SessionManager,
|
||||
log: Logger,
|
||||
) -> anyhow::Result<()> {
|
||||
let service = SessionAdminService::new(session_manager);
|
||||
let incoming = tonic::transport::server::TcpIncoming::bind(addr)
|
||||
.map_err(|e| anyhow::anyhow!("failed to bind TcpIncoming: {}", e))?;
|
||||
|
||||
slog::info!(log, "Admin gRPC server listening on {}", addr);
|
||||
|
||||
Server::builder()
|
||||
.add_service(SessionAdminServer::new(service))
|
||||
.serve_with_incoming(incoming)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Spawn the gRPC server as a background task.
|
||||
pub fn spawn(
|
||||
addr: SocketAddr,
|
||||
session_manager: SessionManager,
|
||||
log: Logger,
|
||||
mut shutdown_rx: broadcast::Receiver<()>,
|
||||
) -> tokio::task::JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let result = serve(addr, session_manager, log).await;
|
||||
if let Err(e) = result {
|
||||
eprintln!("Admin gRPC server error: {}", e);
|
||||
}
|
||||
|
||||
let _ = shutdown_rx.recv().await;
|
||||
})
|
||||
}
|
||||
68
libs/rpc/admin/types.rs
Normal file
68
libs/rpc/admin/types.rs
Normal file
@ -0,0 +1,68 @@
|
||||
//! Conversion between proto types and session_manager types.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use super::generated::admin::{OnlineStatus as ProtoStatus, UserSession as ProtoSession, SessionInfo as ProtoInfo};
|
||||
|
||||
use session_manager::{OnlineStatus, SessionInfo, UserSession};
|
||||
|
||||
/// Convert DateTime<Utc> → prost_types::Timestamp.
|
||||
fn datetime_to_timestamp(dt: DateTime<Utc>) -> prost_types::Timestamp {
|
||||
prost_types::Timestamp {
|
||||
seconds: dt.timestamp(),
|
||||
nanos: dt.timestamp_subsec_nanos() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert session_manager UserSession → proto UserSession.
|
||||
pub fn to_proto_session(s: &UserSession) -> ProtoSession {
|
||||
ProtoSession {
|
||||
session_id: s.session_id.to_string(),
|
||||
user_id: s.user_id.to_string(),
|
||||
workspace_id: s.workspace_id.to_string(),
|
||||
ip_address: s.ip_address.clone(),
|
||||
user_agent: s.user_agent.clone(),
|
||||
connected_at: Some(datetime_to_timestamp(s.connected_at)),
|
||||
last_heartbeat: Some(datetime_to_timestamp(s.last_heartbeat)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert session_manager OnlineStatus → proto OnlineStatus.
|
||||
pub fn to_proto_status(s: OnlineStatus) -> ProtoStatus {
|
||||
match s {
|
||||
OnlineStatus::Online => ProtoStatus::Online,
|
||||
OnlineStatus::Idle => ProtoStatus::Idle,
|
||||
OnlineStatus::Offline => ProtoStatus::Offline,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert session_manager SessionInfo → proto SessionInfo.
|
||||
pub fn to_proto_info(info: &SessionInfo) -> ProtoInfo {
|
||||
ProtoInfo {
|
||||
user_id: info.user_id.to_string(),
|
||||
session_count: info.session_count as u32,
|
||||
workspaces: info.workspaces.iter().map(|w| w.to_string()).collect(),
|
||||
latest_session: info.latest_session.as_ref().map(to_proto_session),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert prost_types::Timestamp → chrono::DateTime<Utc>.
|
||||
pub fn prost_ts_to_chrono(ts: &prost_types::Timestamp) -> DateTime<Utc> {
|
||||
DateTime::from_timestamp(ts.seconds, ts.nanos as u32)
|
||||
.unwrap_or_else(Utc::now)
|
||||
}
|
||||
|
||||
/// Convert proto OnlineStatus → session_manager OnlineStatus.
|
||||
pub fn from_proto_status(s: ProtoStatus) -> OnlineStatus {
|
||||
match s {
|
||||
ProtoStatus::Online => OnlineStatus::Online,
|
||||
ProtoStatus::Idle => OnlineStatus::Idle,
|
||||
ProtoStatus::Offline => OnlineStatus::Offline,
|
||||
ProtoStatus::Unspecified => OnlineStatus::Offline,
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a string UUID. Returns None on parse failure.
|
||||
pub fn parse_uuid(s: &str) -> Option<uuid::Uuid> {
|
||||
uuid::Uuid::parse_str(s).ok()
|
||||
}
|
||||
93
libs/rpc/build.rs
Normal file
93
libs/rpc/build.rs
Normal file
@ -0,0 +1,93 @@
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let proto_file = "proto/admin.proto";
|
||||
let out_dir = "admin/generated";
|
||||
std::fs::create_dir_all(out_dir)?;
|
||||
|
||||
tonic_prost_build::configure()
|
||||
.out_dir(out_dir)
|
||||
.compile_protos(&[proto_file], &["proto/"])?;
|
||||
|
||||
let service = tonic_prost_build::manual::Service::builder()
|
||||
.name("SessionAdmin")
|
||||
.package("admin")
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("list_workspace_sessions")
|
||||
.route_name("ListWorkspaceSessions")
|
||||
.input_type("crate::admin::generated::admin::ListWorkspaceSessionsRequest")
|
||||
.output_type("crate::admin::generated::admin::ListWorkspaceSessionsResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("list_user_sessions")
|
||||
.route_name("ListUserSessions")
|
||||
.input_type("crate::admin::generated::admin::ListUserSessionsRequest")
|
||||
.output_type("crate::admin::generated::admin::ListUserSessionsResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("kick_user_from_workspace")
|
||||
.route_name("KickUserFromWorkspace")
|
||||
.input_type("crate::admin::generated::admin::KickUserFromWorkspaceRequest")
|
||||
.output_type("crate::admin::generated::admin::KickUserFromWorkspaceResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("kick_user")
|
||||
.route_name("KickUser")
|
||||
.input_type("crate::admin::generated::admin::KickUserRequest")
|
||||
.output_type("crate::admin::generated::admin::KickUserResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("get_user_status")
|
||||
.route_name("GetUserStatus")
|
||||
.input_type("crate::admin::generated::admin::GetUserStatusRequest")
|
||||
.output_type("crate::admin::generated::admin::GetUserStatusResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("get_user_info")
|
||||
.route_name("GetUserInfo")
|
||||
.input_type("crate::admin::generated::admin::GetUserInfoRequest")
|
||||
.output_type("crate::admin::generated::admin::GetUserInfoResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("get_workspace_online_users")
|
||||
.route_name("GetWorkspaceOnlineUsers")
|
||||
.input_type("crate::admin::generated::admin::GetWorkspaceOnlineUsersRequest")
|
||||
.output_type("crate::admin::generated::admin::GetWorkspaceOnlineUsersResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.method(
|
||||
tonic_prost_build::manual::Method::builder()
|
||||
.name("is_user_online")
|
||||
.route_name("IsUserOnline")
|
||||
.input_type("crate::admin::generated::admin::IsUserOnlineRequest")
|
||||
.output_type("crate::admin::generated::admin::IsUserOnlineResponse")
|
||||
.codec_path("tonic_prost::ProstCodec")
|
||||
.build(),
|
||||
)
|
||||
.build();
|
||||
|
||||
tonic_prost_build::manual::Builder::new()
|
||||
.out_dir(out_dir)
|
||||
.build_transport(true)
|
||||
.compile(&[service]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -1,14 +1 @@
|
||||
pub fn add(left: u64, right: u64) -> u64 {
|
||||
left + right
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
}
|
||||
}
|
||||
pub mod admin;
|
||||
|
||||
109
libs/rpc/proto/admin.proto
Normal file
109
libs/rpc/proto/admin.proto
Normal file
@ -0,0 +1,109 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package admin;
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session entities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message UserSession {
|
||||
string session_id = 1;
|
||||
string user_id = 2;
|
||||
string workspace_id = 3;
|
||||
optional string ip_address = 4;
|
||||
optional string user_agent = 5;
|
||||
google.protobuf.Timestamp connected_at = 6;
|
||||
google.protobuf.Timestamp last_heartbeat = 7;
|
||||
}
|
||||
|
||||
message SessionInfo {
|
||||
string user_id = 1;
|
||||
uint32 session_count = 2;
|
||||
repeated string workspaces = 3;
|
||||
optional UserSession latest_session = 4;
|
||||
}
|
||||
|
||||
enum OnlineStatus {
|
||||
ONLINE_STATUS_UNSPECIFIED = 0;
|
||||
ONLINE_STATUS_ONLINE = 1;
|
||||
ONLINE_STATUS_IDLE = 2;
|
||||
ONLINE_STATUS_OFFLINE = 3;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requests & Responses
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
message ListWorkspaceSessionsRequest {
|
||||
string workspace_id = 1;
|
||||
}
|
||||
message ListWorkspaceSessionsResponse {
|
||||
repeated UserSession sessions = 1;
|
||||
}
|
||||
|
||||
message ListUserSessionsRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
message ListUserSessionsResponse {
|
||||
repeated UserSession sessions = 1;
|
||||
}
|
||||
|
||||
message KickUserFromWorkspaceRequest {
|
||||
string user_id = 1;
|
||||
string workspace_id = 2;
|
||||
}
|
||||
message KickUserFromWorkspaceResponse {
|
||||
uint32 kicked_count = 1;
|
||||
}
|
||||
|
||||
message KickUserRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
message KickUserResponse {
|
||||
uint32 kicked_count = 1;
|
||||
}
|
||||
|
||||
message GetUserStatusRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
message GetUserStatusResponse {
|
||||
OnlineStatus status = 1;
|
||||
}
|
||||
|
||||
message GetUserInfoRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
message GetUserInfoResponse {
|
||||
optional SessionInfo info = 1;
|
||||
}
|
||||
|
||||
message GetWorkspaceOnlineUsersRequest {
|
||||
string workspace_id = 1;
|
||||
}
|
||||
message GetWorkspaceOnlineUsersResponse {
|
||||
repeated string user_ids = 1;
|
||||
}
|
||||
|
||||
message IsUserOnlineRequest {
|
||||
string user_id = 1;
|
||||
}
|
||||
message IsUserOnlineResponse {
|
||||
bool online = 1;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Service
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
service SessionAdmin {
|
||||
rpc ListWorkspaceSessions(ListWorkspaceSessionsRequest) returns (ListWorkspaceSessionsResponse);
|
||||
rpc ListUserSessions(ListUserSessionsRequest) returns (ListUserSessionsResponse);
|
||||
rpc KickUserFromWorkspace(KickUserFromWorkspaceRequest) returns (KickUserFromWorkspaceResponse);
|
||||
rpc KickUser(KickUserRequest) returns (KickUserResponse);
|
||||
rpc GetUserStatus(GetUserStatusRequest) returns (GetUserStatusResponse);
|
||||
rpc GetUserInfo(GetUserInfoRequest) returns (GetUserInfoResponse);
|
||||
rpc GetWorkspaceOnlineUsers(GetWorkspaceOnlineUsersRequest) returns (GetWorkspaceOnlineUsersResponse);
|
||||
rpc IsUserOnline(IsUserOnlineRequest) returns (IsUserOnlineResponse);
|
||||
}
|
||||
32
libs/session_manager/Cargo.toml
Normal file
32
libs/session_manager/Cargo.toml
Normal file
@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "session_manager"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
description.workspace = true
|
||||
repository.workspace = true
|
||||
readme.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
documentation.workspace = true
|
||||
[lib]
|
||||
path = "src/lib.rs"
|
||||
name = "session_manager"
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
deadpool-redis = { workspace = true, features = ["cluster"] }
|
||||
rand = { workspace = true }
|
||||
redis = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "sync"] }
|
||||
uuid = { workspace = true, features = ["serde", "v4"] }
|
||||
slog = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
7
libs/session_manager/src/lib.rs
Normal file
7
libs/session_manager/src/lib.rs
Normal file
@ -0,0 +1,7 @@
|
||||
mod manager;
|
||||
mod storage;
|
||||
mod types;
|
||||
|
||||
pub use manager::{SessionManager, SessionManagerConfig};
|
||||
pub use storage::SessionStorage;
|
||||
pub use types::{OnlineStatus, SessionInfo, UserSession};
|
||||
197
libs/session_manager/src/manager.rs
Normal file
197
libs/session_manager/src/manager.rs
Normal file
@ -0,0 +1,197 @@
|
||||
use chrono::Utc;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::storage::{SessionStorage, SessionStorageError};
|
||||
use crate::types::{OnlineStatus, SessionInfo, UserSession};
|
||||
use slog::info;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionManagerConfig {
|
||||
pub heartbeat_interval_secs: u64,
|
||||
pub idle_threshold_secs: u64,
|
||||
}
|
||||
|
||||
impl Default for SessionManagerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
heartbeat_interval_secs: 60,
|
||||
idle_threshold_secs: 300,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionManager {
|
||||
storage: SessionStorage,
|
||||
#[allow(dead_code)]
|
||||
config: SessionManagerConfig,
|
||||
logger: slog::Logger,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
pub fn new(storage: SessionStorage, logger: slog::Logger) -> Self {
|
||||
Self {
|
||||
storage,
|
||||
config: SessionManagerConfig::default(),
|
||||
logger,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_config(
|
||||
storage: SessionStorage,
|
||||
config: SessionManagerConfig,
|
||||
logger: slog::Logger,
|
||||
) -> Self {
|
||||
Self {
|
||||
storage,
|
||||
config,
|
||||
logger,
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a new user session. Returns the generated session ID.
|
||||
pub async fn register_session(
|
||||
&self,
|
||||
user_id: Uuid,
|
||||
workspace_id: Uuid,
|
||||
ip_address: Option<String>,
|
||||
user_agent: Option<String>,
|
||||
) -> Result<UserSession, SessionStorageError> {
|
||||
let now = Utc::now();
|
||||
let session = UserSession {
|
||||
session_id: Uuid::new_v4(),
|
||||
user_id,
|
||||
workspace_id,
|
||||
ip_address,
|
||||
user_agent,
|
||||
connected_at: now,
|
||||
last_heartbeat: now,
|
||||
};
|
||||
|
||||
self.storage.save_session(&session).await?;
|
||||
info!(self.logger, "session_registered";
|
||||
"session_id" => %session.session_id,
|
||||
"user_id" => %session.user_id,
|
||||
"workspace_id" => %session.workspace_id
|
||||
);
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
/// Refresh a session's heartbeat to keep it alive.
|
||||
pub async fn heartbeat(&self, session_id: &Uuid) -> Result<(), SessionStorageError> {
|
||||
self.storage.heartbeat(session_id).await
|
||||
}
|
||||
|
||||
/// Remove a single session (logout from one device/tab).
|
||||
pub async fn remove_session(&self, session_id: &Uuid) -> Result<(), SessionStorageError> {
|
||||
self.storage.delete_session(session_id).await?;
|
||||
info!(self.logger, "session_removed"; "session_id" => %session_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Kick a user from a workspace (remove all their sessions in that workspace).
|
||||
pub async fn kick_user_from_workspace(
|
||||
&self,
|
||||
user_id: &Uuid,
|
||||
workspace_id: &Uuid,
|
||||
) -> Result<usize, SessionStorageError> {
|
||||
let deleted = self
|
||||
.storage
|
||||
.delete_user_workspace_sessions(user_id, workspace_id)
|
||||
.await?;
|
||||
let count = deleted.len();
|
||||
info!(self.logger, "user_kicked_from_workspace";
|
||||
"user_id" => %user_id,
|
||||
"workspace_id" => %workspace_id,
|
||||
"sessions_removed" => count
|
||||
);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Kick a user from all workspaces (full logout across all devices).
|
||||
pub async fn kick_user(&self, user_id: &Uuid) -> Result<usize, SessionStorageError> {
|
||||
let deleted = self.storage.delete_user_sessions(user_id).await?;
|
||||
let count = deleted.len();
|
||||
info!(self.logger, "user_kicked"; "user_id" => %user_id, "sessions_removed" => count);
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Get all sessions for a user.
|
||||
pub async fn get_user_sessions(
|
||||
&self,
|
||||
user_id: &Uuid,
|
||||
) -> Result<Vec<UserSession>, SessionStorageError> {
|
||||
self.storage.get_user_sessions(user_id).await
|
||||
}
|
||||
|
||||
/// Get session info (summary) for a user.
|
||||
pub async fn get_user_info(
|
||||
&self,
|
||||
user_id: &Uuid,
|
||||
) -> Result<Option<SessionInfo>, SessionStorageError> {
|
||||
let sessions = self.storage.get_user_sessions(user_id).await?;
|
||||
if sessions.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let mut workspaces: Vec<Uuid> = sessions.iter().map(|s| s.workspace_id).collect();
|
||||
workspaces.sort();
|
||||
workspaces.dedup();
|
||||
|
||||
let latest = sessions.iter().max_by_key(|s| s.connected_at).cloned();
|
||||
|
||||
Ok(Some(SessionInfo {
|
||||
user_id: *user_id,
|
||||
session_count: sessions.len(),
|
||||
workspaces,
|
||||
latest_session: latest,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Get all active sessions in a workspace.
|
||||
pub async fn get_workspace_sessions(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
) -> Result<Vec<UserSession>, SessionStorageError> {
|
||||
self.storage.get_workspace_sessions(workspace_id).await
|
||||
}
|
||||
|
||||
/// Get distinct user IDs in a workspace.
|
||||
pub async fn get_workspace_online_users(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
) -> Result<Vec<Uuid>, SessionStorageError> {
|
||||
self.storage.get_workspace_online_users(workspace_id).await
|
||||
}
|
||||
|
||||
/// Get online status for a user.
|
||||
pub async fn get_user_status(
|
||||
&self,
|
||||
user_id: &Uuid,
|
||||
) -> Result<OnlineStatus, SessionStorageError> {
|
||||
self.storage.get_user_status(user_id).await
|
||||
}
|
||||
|
||||
/// Check if a user is online in any workspace.
|
||||
pub async fn is_user_online(&self, user_id: &Uuid) -> Result<bool, SessionStorageError> {
|
||||
self.storage.is_user_online(user_id).await
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying Redis pool.
|
||||
pub fn pool(&self) -> &deadpool_redis::cluster::Pool {
|
||||
self.storage.pool()
|
||||
}
|
||||
|
||||
/// Get online status for multiple users at once.
|
||||
pub async fn get_bulk_status(
|
||||
&self,
|
||||
user_ids: &[Uuid],
|
||||
) -> Result<Vec<(Uuid, OnlineStatus)>, SessionStorageError> {
|
||||
let mut results = Vec::with_capacity(user_ids.len());
|
||||
for uid in user_ids {
|
||||
let status = self.storage.get_user_status(uid).await?;
|
||||
results.push((*uid, status));
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
329
libs/session_manager/src/storage.rs
Normal file
329
libs/session_manager/src/storage.rs
Normal file
@ -0,0 +1,329 @@
|
||||
use anyhow::Context;
|
||||
use chrono::Utc;
|
||||
use deadpool_redis::cluster::Pool;
|
||||
use redis::AsyncCommands;
|
||||
use serde_json;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::types::UserSession;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SessionStorageError {
|
||||
#[error("Redis error: {0}")]
|
||||
Redis(#[from] anyhow::Error),
|
||||
|
||||
#[error("session not found: {0}")]
|
||||
NotFound(Uuid),
|
||||
}
|
||||
|
||||
const KEY_CONN: &str = "user:conn:";
|
||||
const KEY_USER_SESSIONS: &str = "user:user_sessions:";
|
||||
const KEY_WORKSPACE_SESSIONS: &str = "user:workspace_sessions:";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SessionStorage {
|
||||
pool: Pool,
|
||||
heartbeat_ttl_secs: u64,
|
||||
}
|
||||
|
||||
impl SessionStorage {
|
||||
pub fn new(pool: Pool) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
heartbeat_ttl_secs: 120,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_heartbeat_ttl(mut self, ttl_secs: u64) -> Self {
|
||||
self.heartbeat_ttl_secs = ttl_secs;
|
||||
self
|
||||
}
|
||||
|
||||
async fn get_conn(&self) -> Result<deadpool_redis::cluster::Connection, SessionStorageError> {
|
||||
self.pool
|
||||
.get()
|
||||
.await
|
||||
.context("failed to get Redis connection from pool")
|
||||
.map_err(SessionStorageError::Redis)
|
||||
}
|
||||
|
||||
fn conn_key(session_id: &Uuid) -> String {
|
||||
format!("{KEY_CONN}{session_id}")
|
||||
}
|
||||
|
||||
fn user_sessions_key(user_id: &Uuid) -> String {
|
||||
format!("{KEY_USER_SESSIONS}{user_id}")
|
||||
}
|
||||
|
||||
fn workspace_sessions_key(workspace_id: &Uuid) -> String {
|
||||
format!("{KEY_WORKSPACE_SESSIONS}{workspace_id}")
|
||||
}
|
||||
|
||||
fn to_err<E: std::error::Error + Send + Sync + 'static>(e: E) -> SessionStorageError {
|
||||
SessionStorageError::Redis(anyhow::anyhow!(e))
|
||||
}
|
||||
|
||||
/// Store a new user session and associate it with user + workspace indexes.
|
||||
pub async fn save_session(&self, session: &UserSession) -> Result<(), SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let key = Self::conn_key(&session.session_id);
|
||||
let user_key = Self::user_sessions_key(&session.user_id);
|
||||
let ws_key = Self::workspace_sessions_key(&session.workspace_id);
|
||||
|
||||
let value = serde_json::to_string(session)
|
||||
.context("serialize UserSession")
|
||||
.map_err(SessionStorageError::Redis)?;
|
||||
|
||||
let ttl = self.heartbeat_ttl_secs;
|
||||
|
||||
let _: () = redis::pipe()
|
||||
.set_ex(&key, &value, ttl)
|
||||
.sadd(&user_key, session_id_str(&session.session_id))
|
||||
.expire(&user_key, 0)
|
||||
.sadd(&ws_key, session_id_str(&session.session_id))
|
||||
.expire(&ws_key, 0)
|
||||
.query_async(&mut conn)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get a session by its ID.
|
||||
pub async fn get_session(&self, session_id: &Uuid) -> Result<Option<UserSession>, SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let key = Self::conn_key(session_id);
|
||||
|
||||
let value: Option<String> = conn
|
||||
.get(&key)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
match value {
|
||||
Some(v) => {
|
||||
let session: UserSession = serde_json::from_str(&v)
|
||||
.context("deserialize UserSession")
|
||||
.map_err(SessionStorageError::Redis)?;
|
||||
Ok(Some(session))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the heartbeat timestamp and refresh TTL.
|
||||
pub async fn heartbeat(&self, session_id: &Uuid) -> Result<(), SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let key = Self::conn_key(session_id);
|
||||
let ttl = self.heartbeat_ttl_secs;
|
||||
let updated = Utc::now();
|
||||
|
||||
let script = redis::Script::new(
|
||||
r#"
|
||||
local v = redis.call('GET', KEYS[1])
|
||||
if not v then return 0 end
|
||||
local session = cjson.decode(v)
|
||||
session.last_heartbeat = ARGV[1]
|
||||
redis.call('SETEX', KEYS[1], ARGV[2], cjson.encode(session))
|
||||
return 1
|
||||
"#,
|
||||
);
|
||||
|
||||
let result: i64 = script
|
||||
.key(&key)
|
||||
.arg(updated.to_rfc3339())
|
||||
.arg(ttl)
|
||||
.invoke_async(&mut conn)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
if result == 0 {
|
||||
return Err(SessionStorageError::NotFound(*session_id));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete a session by ID and clean up indexes.
|
||||
pub async fn delete_session(&self, session_id: &Uuid) -> Result<(), SessionStorageError> {
|
||||
let session = self.get_session(session_id).await?;
|
||||
|
||||
let key = Self::conn_key(session_id);
|
||||
|
||||
let _: () = self
|
||||
.get_conn()
|
||||
.await?
|
||||
.del(&key)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
if let Some(ref s) = session {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let user_key = Self::user_sessions_key(&s.user_id);
|
||||
let ws_key = Self::workspace_sessions_key(&s.workspace_id);
|
||||
let id_str = session_id_str(session_id);
|
||||
let _: () = conn.srem::<_, _, ()>(&user_key, &id_str).await.map_err(Self::to_err)?;
|
||||
let _: () = conn.srem::<_, _, ()>(&ws_key, &id_str).await.map_err(Self::to_err)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Delete all sessions for a specific user.
|
||||
pub async fn delete_user_sessions(&self, user_id: &Uuid) -> Result<Vec<Uuid>, SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let user_key = Self::user_sessions_key(user_id);
|
||||
|
||||
let session_ids: Vec<String> = conn
|
||||
.smembers(&user_key)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
let mut deleted = Vec::new();
|
||||
for id_str in &session_ids {
|
||||
if let Ok(sid) = Uuid::parse_str(id_str) {
|
||||
let conn_key = Self::conn_key(&sid);
|
||||
let _: () = conn.del(&conn_key).await.map_err(Self::to_err)?;
|
||||
deleted.push(sid);
|
||||
}
|
||||
}
|
||||
|
||||
let _: () = conn.del(&user_key).await.map_err(Self::to_err)?;
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Delete all sessions for a user within a specific workspace.
|
||||
pub async fn delete_user_workspace_sessions(
|
||||
&self,
|
||||
user_id: &Uuid,
|
||||
workspace_id: &Uuid,
|
||||
) -> Result<Vec<Uuid>, SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let ws_key = Self::workspace_sessions_key(workspace_id);
|
||||
let user_key = Self::user_sessions_key(user_id);
|
||||
|
||||
let ws_session_ids: Vec<String> = conn
|
||||
.smembers(&ws_key)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
let mut deleted = Vec::new();
|
||||
for id_str in &ws_session_ids {
|
||||
if let Ok(sid) = Uuid::parse_str(id_str) {
|
||||
let session = self.get_session(&sid).await?;
|
||||
if let Some(ref s) = session {
|
||||
if s.user_id == *user_id {
|
||||
let conn_key = Self::conn_key(&sid);
|
||||
let _: () = conn.del(&conn_key).await.map_err(Self::to_err)?;
|
||||
let _: () = conn.srem::<_, _, ()>(&user_key, id_str).await.map_err(Self::to_err)?;
|
||||
deleted.push(sid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(deleted)
|
||||
}
|
||||
|
||||
/// Get all active sessions for a user.
|
||||
pub async fn get_user_sessions(&self, user_id: &Uuid) -> Result<Vec<UserSession>, SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let user_key = Self::user_sessions_key(user_id);
|
||||
|
||||
let session_ids: Vec<String> = conn
|
||||
.smembers(&user_key)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
for id_str in &session_ids {
|
||||
if let Ok(sid) = Uuid::parse_str(id_str) {
|
||||
if let Ok(Some(session)) = self.get_session(&sid).await {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
/// Get all active sessions in a workspace.
|
||||
pub async fn get_workspace_sessions(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
) -> Result<Vec<UserSession>, SessionStorageError> {
|
||||
let mut conn = self.get_conn().await?;
|
||||
let ws_key = Self::workspace_sessions_key(workspace_id);
|
||||
|
||||
let session_ids: Vec<String> = conn
|
||||
.smembers(&ws_key)
|
||||
.await
|
||||
.map_err(Self::to_err)?;
|
||||
|
||||
let mut sessions = Vec::new();
|
||||
for id_str in &session_ids {
|
||||
if let Ok(sid) = Uuid::parse_str(id_str) {
|
||||
if let Ok(Some(session)) = self.get_session(&sid).await {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
/// Get distinct user IDs active in a workspace.
|
||||
pub async fn get_workspace_online_users(
|
||||
&self,
|
||||
workspace_id: &Uuid,
|
||||
) -> Result<Vec<Uuid>, SessionStorageError> {
|
||||
let sessions = self.get_workspace_sessions(workspace_id).await?;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut result = Vec::new();
|
||||
for s in sessions {
|
||||
if seen.insert(s.user_id) {
|
||||
result.push(s.user_id);
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Get the count of online sessions for a user.
|
||||
pub async fn get_user_session_count(&self, user_id: &Uuid) -> Result<usize, SessionStorageError> {
|
||||
let sessions = self.get_user_sessions(user_id).await?;
|
||||
Ok(sessions.len())
|
||||
}
|
||||
|
||||
/// Check if a user has any active sessions (online status).
|
||||
pub async fn is_user_online(&self, user_id: &Uuid) -> Result<bool, SessionStorageError> {
|
||||
let count = self.get_user_session_count(user_id).await?;
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying Redis pool.
|
||||
pub fn pool(&self) -> &deadpool_redis::cluster::Pool {
|
||||
&self.pool
|
||||
}
|
||||
|
||||
/// Get online status for a user.
|
||||
pub async fn get_user_status(&self, user_id: &Uuid) -> Result<crate::types::OnlineStatus, SessionStorageError> {
|
||||
let sessions = self.get_user_sessions(user_id).await?;
|
||||
if sessions.is_empty() {
|
||||
return Ok(crate::types::OnlineStatus::Offline);
|
||||
}
|
||||
|
||||
let now = Utc::now();
|
||||
let idle_threshold = chrono::Duration::minutes(5);
|
||||
|
||||
if sessions.iter().any(|s| now - s.last_heartbeat < idle_threshold) {
|
||||
Ok(crate::types::OnlineStatus::Online)
|
||||
} else {
|
||||
Ok(crate::types::OnlineStatus::Idle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn session_id_str(id: &Uuid) -> String {
|
||||
id.to_string()
|
||||
}
|
||||
36
libs/session_manager/src/types.rs
Normal file
36
libs/session_manager/src/types.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OnlineStatus {
|
||||
Online,
|
||||
Idle,
|
||||
Offline,
|
||||
}
|
||||
|
||||
impl Default for OnlineStatus {
|
||||
fn default() -> Self {
|
||||
Self::Offline
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserSession {
|
||||
pub session_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub workspace_id: Uuid,
|
||||
pub ip_address: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub connected_at: DateTime<Utc>,
|
||||
pub last_heartbeat: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionInfo {
|
||||
pub user_id: Uuid,
|
||||
pub session_count: usize,
|
||||
pub workspaces: Vec<Uuid>,
|
||||
pub latest_session: Option<UserSession>,
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user