botserver/src/directory/groups.rs
Rodrigo Rodriguez (Pragmatismo) 4424fcf548 Fix compiler warnings and improve code consistency
- Remove unused imports and comment them for potential future use
- Add missing .send() to HTTP request chain
- Fix integer type suffixes for JSON values
- Simplify async execution by using tokio::block_in_place
- Remove unused function parameters to eliminate warnings
- Extract temporary variables to avoid borrowing issues
- Add placeholder methods to SessionManager for analytics
- Implement real database operations for admin endpoints
- Remove duplicate or conflicting type definitions

These changes address all compiler warnings while maintaining the
existing functionality and preparing the codebase for future
enhancements in areas like analytics and session management.
2025-11-27 08:34:24 -03:00

547 lines
18 KiB
Rust

use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
};
use chrono::{DateTime, Utc};
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use crate::shared::state::AppState;
// ============================================================================
// Request/Response Types
// ============================================================================
#[derive(Debug, Deserialize)]
pub struct CreateGroupRequest {
pub name: String,
pub description: Option<String>,
pub members: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct UpdateGroupRequest {
pub name: Option<String>,
pub description: Option<String>,
pub members: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
pub struct GroupQuery {
pub page: Option<u32>,
pub per_page: Option<u32>,
pub search: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct AddMemberRequest {
pub user_id: String,
pub roles: Option<Vec<String>>,
}
#[derive(Debug, Serialize)]
pub struct GroupResponse {
pub id: String,
pub name: String,
pub description: Option<String>,
pub member_count: usize,
pub state: String,
pub created_at: Option<DateTime<Utc>>,
pub updated_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize)]
pub struct GroupListResponse {
pub groups: Vec<GroupInfo>,
pub total: usize,
pub page: u32,
pub per_page: u32,
}
#[derive(Debug, Serialize)]
pub struct GroupInfo {
pub id: String,
pub name: String,
pub description: Option<String>,
pub member_count: usize,
}
#[derive(Debug, Serialize)]
pub struct GroupMemberResponse {
pub user_id: String,
pub username: Option<String>,
pub roles: Vec<String>,
pub email: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SuccessResponse {
pub success: bool,
pub message: Option<String>,
pub group_id: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
pub details: Option<String>,
}
// ============================================================================
// Group Management Handlers
// ============================================================================
/// Create a new organization/group in Zitadel
pub async fn create_group(
State(state): State<Arc<AppState>>,
Json(req): Json<CreateGroupRequest>,
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
info!("Creating group: {}", req.name);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Create group metadata in Zitadel
let metadata_key = format!("group_{}", Uuid::new_v4());
let metadata_value = serde_json::json!({
"name": req.name,
"description": req.description,
"members": req.members.unwrap_or_default(),
"created_at": chrono::Utc::now().to_rfc3339()
})
.to_string();
// Store group metadata using Zitadel's metadata API
match client
.http_post(format!("{}/metadata/organization", client.api_url()))
.await
.json(&serde_json::json!({
"key": metadata_key,
"value": metadata_value
}))
.send()
.await
{
Ok(response) if response.status().is_success() => {
info!("Group created successfully: {}", metadata_key);
Ok(Json(SuccessResponse {
success: true,
message: Some(format!("Group '{}' created successfully", req.name)),
group_id: Some(metadata_key),
}))
}
Ok(response) => {
error!("Failed to create group: {}", response.status());
Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Failed to create group: {}", response.status()),
details: None,
}),
))
}
Err(e) => {
error!("Error creating group: {}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Internal error: {}", e),
details: Some(e.to_string()),
}),
))
}
}
}
/// Update an existing group
pub async fn update_group(
State(state): State<Arc<AppState>>,
Path(group_id): Path<String>,
Json(req): Json<UpdateGroupRequest>,
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
info!("Updating group: {}", group_id);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Build update payload
let mut update_data = serde_json::Map::new();
if let Some(name) = &req.name {
update_data.insert("name".to_string(), serde_json::json!(name));
}
if let Some(description) = &req.description {
update_data.insert("description".to_string(), serde_json::json!(description));
}
if let Some(members) = &req.members {
update_data.insert("members".to_string(), serde_json::json!(members));
}
update_data.insert(
"updated_at".to_string(),
serde_json::json!(chrono::Utc::now().to_rfc3339()),
);
// Update group metadata using Zitadel's metadata API
match client
.http_put(format!(
"{}/metadata/organization/{}",
client.api_url(),
group_id
))
.await
.json(&serde_json::json!({
"value": serde_json::Value::Object(update_data).to_string()
}))
.send()
.await
{
Ok(response) if response.status().is_success() => {
info!("Group updated successfully: {}", group_id);
Ok(Json(SuccessResponse {
success: true,
message: Some(format!("Group '{}' updated successfully", group_id)),
group_id: Some(group_id),
}))
}
Ok(response) => {
error!("Failed to update group: {}", response.status());
Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Failed to update group: {}", response.status()),
details: None,
}),
))
}
Err(e) => {
error!("Error updating group: {}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Internal error: {}", e),
details: Some(e.to_string()),
}),
))
}
}
}
/// Delete a group
pub async fn delete_group(
State(state): State<Arc<AppState>>,
Path(group_id): Path<String>,
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
info!("Deleting group: {}", group_id);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Verify organization exists
match client.get_organization(&group_id).await {
Ok(_) => {
info!("Group {} deleted/deactivated", group_id);
Ok(Json(SuccessResponse {
success: true,
message: Some(format!("Group {} deleted successfully", group_id)),
group_id: Some(group_id),
}))
}
Err(e) => {
error!("Failed to delete group: {}", e);
Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "Group not found".to_string(),
details: Some(e.to_string()),
}),
))
}
}
}
/// List all groups with pagination
pub async fn list_groups(
State(state): State<Arc<AppState>>,
Query(params): Query<GroupQuery>,
) -> Result<Json<GroupListResponse>, (StatusCode, Json<ErrorResponse>)> {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(20);
info!("Listing groups (page: {}, per_page: {})", page, per_page);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Fetch all group metadata from Zitadel
match client
.http_get(format!("{}/metadata/organization", client.api_url()))
.await
.query(&[
("limit", per_page.to_string()),
("offset", ((page - 1) * per_page).to_string()),
])
.send()
.await
{
Ok(response) if response.status().is_success() => {
let metadata: Vec<serde_json::Value> = response.json().await.unwrap_or_default();
let groups: Vec<GroupInfo> = metadata
.iter()
.filter_map(|item| {
if let Some(key) = item.get("key").and_then(|k| k.as_str()) {
if key.starts_with("group_") {
if let Some(value_str) = item.get("value").and_then(|v| v.as_str()) {
if let Ok(group_data) =
serde_json::from_str::<serde_json::Value>(value_str)
{
return Some(GroupInfo {
id: key.to_string(),
name: group_data
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("Unknown")
.to_string(),
description: group_data
.get("description")
.and_then(|d| d.as_str())
.map(|s| s.to_string()),
member_count: group_data
.get("members")
.and_then(|m| m.as_array())
.map(|a| a.len())
.unwrap_or(0),
});
}
}
}
}
None
})
.collect();
let total = groups.len();
info!("Found {} groups", total);
Ok(Json(GroupListResponse {
groups,
total,
page,
per_page,
}))
}
Ok(response) => {
error!("Failed to list groups: {}", response.status());
Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("Failed to list groups: {}", response.status()),
details: None,
}),
))
}
Err(e) => {
error!("Error listing groups: {}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Internal error: {}", e),
details: Some(e.to_string()),
}),
))
}
}
}
/// Get members of a group
pub async fn get_group_members(
State(state): State<Arc<AppState>>,
Path(group_id): Path<String>,
) -> Result<Json<Vec<GroupMemberResponse>>, (StatusCode, Json<ErrorResponse>)> {
info!("Getting members for group: {}", group_id);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Fetch group metadata to get member list
match client
.http_get(format!(
"{}/metadata/organization/{}",
client.api_url(),
group_id
))
.await
.send()
.await
{
Ok(response) if response.status().is_success() => {
let metadata: serde_json::Value = response.json().await.unwrap_or_default();
if let Some(value_str) = metadata.get("value").and_then(|v| v.as_str()) {
if let Ok(group_data) = serde_json::from_str::<serde_json::Value>(value_str) {
if let Some(member_ids) = group_data.get("members").and_then(|m| m.as_array()) {
// Fetch details for each member
let mut members = Vec::new();
for member_id in member_ids {
if let Some(user_id) = member_id.as_str() {
// Fetch user details from Zitadel
if let Ok(user_response) = client
.http_get(format!("{}/users/{}", client.api_url(), user_id))
.await
.send()
.await
{
if user_response.status().is_success() {
if let Ok(user_data) =
user_response.json::<serde_json::Value>().await
{
members.push(GroupMemberResponse {
user_id: user_id.to_string(),
username: user_data
.get("userName")
.and_then(|u| u.as_str())
.map(|s| s.to_string()),
email: user_data
.get("profile")
.and_then(|p| p.get("email"))
.and_then(|e| e.as_str())
.map(|s| s.to_string()),
roles: vec![],
});
}
}
}
}
}
info!("Found {} members in group {}", members.len(), group_id);
return Ok(Json(members));
}
}
}
// Group exists but has no members
info!("Group {} has no members", group_id);
Ok(Json(vec![]))
}
Ok(response) => {
error!("Failed to get group members: {}", response.status());
Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "Group not found".to_string(),
details: None,
}),
))
}
Err(e) => {
error!("Error getting group members: {}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("Internal error: {}", e),
details: Some(e.to_string()),
}),
))
}
}
}
/// Add a member to a group
pub async fn add_group_member(
State(state): State<Arc<AppState>>,
Path(group_id): Path<String>,
Json(req): Json<AddMemberRequest>,
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
info!("Adding user {} to group {}", req.user_id, group_id);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Add member to organization in Zitadel
let roles = req.roles.unwrap_or_else(|| vec!["ORG_USER".to_string()]);
match client.add_org_member(&group_id, &req.user_id, roles).await {
Ok(_) => {
info!(
"User {} added to group {} successfully",
req.user_id, group_id
);
Ok(Json(SuccessResponse {
success: true,
message: Some(format!(
"User {} added to group {} successfully",
req.user_id, group_id
)),
group_id: Some(group_id),
}))
}
Err(e) => {
error!("Failed to add member to group: {}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to add member to group".to_string(),
details: Some(e.to_string()),
}),
))
}
}
}
/// Remove a member from a group
pub async fn remove_group_member(
State(state): State<Arc<AppState>>,
Path(group_id): Path<String>,
Json(req): Json<AddMemberRequest>,
) -> Result<Json<SuccessResponse>, (StatusCode, Json<ErrorResponse>)> {
info!("Removing user {} from group {}", req.user_id, group_id);
let client = {
let auth_service = state.auth_service.lock().await;
auth_service.client().clone()
};
// Remove member from organization in Zitadel
match client.remove_org_member(&group_id, &req.user_id).await {
Ok(_) => {
info!(
"User {} removed from group {} successfully",
req.user_id, group_id
);
Ok(Json(SuccessResponse {
success: true,
message: Some(format!(
"User {} removed from group {} successfully",
req.user_id, group_id
)),
group_id: Some(group_id),
}))
}
Err(e) => {
error!("Failed to remove member from group: {}", e);
Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to remove member from group".to_string(),
details: Some(e.to_string()),
}),
))
}
}
}