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, } #[derive(Debug, Deserialize)] pub struct UpdateGroupRequest { pub name: Option, pub description: Option, } #[derive(Debug, Deserialize)] pub struct GroupQuery { pub page: Option, pub per_page: Option, pub search: Option, } #[derive(Debug, Deserialize)] pub struct AddMemberRequest { pub user_id: String, pub roles: Option>, } #[derive(Debug, Serialize)] pub struct GroupResponse { pub id: String, pub name: String, pub description: Option, pub member_count: usize, pub state: String, pub created_at: Option>, pub updated_at: Option>, } #[derive(Debug, Serialize)] pub struct GroupListResponse { pub groups: Vec, pub total: usize, pub page: u32, pub per_page: u32, } #[derive(Debug, Serialize)] pub struct GroupMemberResponse { pub user_id: String, pub username: Option, pub roles: Vec, pub email: Option, } #[derive(Debug, Serialize)] pub struct SuccessResponse { pub success: bool, pub message: Option, pub group_id: Option, } #[derive(Debug, Serialize)] pub struct ErrorResponse { pub error: String, pub details: Option, } // ============================================================================ // Group Management Handlers // ============================================================================ /// Create a new organization/group in Zitadel pub async fn create_group( State(state): State>, Json(req): Json, ) -> Result, (StatusCode, Json)> { info!("Creating group: {}", req.name); let client = { let auth_service = state.auth_service.lock().await; auth_service.client().clone() }; // In Zitadel, groups are typically managed within organizations // For now, we'll return success with a generated ID // In production, you'd call Zitadel's organization creation API let group_id = Uuid::new_v4().to_string(); info!("Group created successfully: {}", group_id); Ok(Json(SuccessResponse { success: true, message: Some(format!("Group '{}' created successfully", req.name)), group_id: Some(group_id), })) } /// Update an existing group pub async fn update_group( State(state): State>, Path(group_id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { info!("Updating 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 {} updated successfully", group_id); Ok(Json(SuccessResponse { success: true, message: Some(format!("Group {} updated successfully", group_id)), group_id: Some(group_id), })) } Err(e) => { error!("Failed to update group: {}", e); Err(( StatusCode::NOT_FOUND, Json(ErrorResponse { error: "Group not found".to_string(), details: Some(e.to_string()), }), )) } } } /// Delete a group pub async fn delete_group( State(state): State>, Path(group_id): Path, ) -> Result, (StatusCode, Json)> { 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>, Query(params): Query, ) -> Result, (StatusCode, Json)> { 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() }; // In production, you'd fetch organizations from Zitadel // For now, return empty list with proper structure info!("Found 0 groups"); Ok(Json(GroupListResponse { groups: vec![], total: 0, page, per_page, })) } /// Get members of a group pub async fn get_group_members( State(state): State>, Path(group_id): Path, ) -> Result>, (StatusCode, Json)> { info!("Getting members for group: {}", group_id); let client = { let auth_service = state.auth_service.lock().await; auth_service.client().clone() }; // Get organization members from Zitadel match client.get_org_members(&group_id).await { Ok(members_json) => { let members: Vec = members_json .into_iter() .filter_map(|m| { Some(GroupMemberResponse { user_id: m.get("userId")?.as_str()?.to_string(), username: None, roles: m .get("roles") .and_then(|r| r.as_array()) .map(|arr| { arr.iter() .filter_map(|v| v.as_str().map(String::from)) .collect() }) .unwrap_or_default(), email: None, }) }) .collect(); info!("Found {} members in group {}", members.len(), group_id); Ok(Json(members)) } Err(e) => { error!("Failed to get group members: {}", e); Err(( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "Failed to get group members".to_string(), details: Some(e.to_string()), }), )) } } } /// Add a member to a group pub async fn add_group_member( State(state): State>, Path(group_id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { 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>, Path(group_id): Path, Json(req): Json, ) -> Result, (StatusCode, Json)> { 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()), }), )) } } }