feat: Implement admin team management endpoints with permissions and DTOs
This commit is contained in:
@@ -20,6 +20,8 @@ pub enum PermissionsEnum {
|
|||||||
CreatePermissions,
|
CreatePermissions,
|
||||||
DeletePermissions,
|
DeletePermissions,
|
||||||
UpdatePermissions,
|
UpdatePermissions,
|
||||||
|
ReadListTeams,
|
||||||
|
ReadDetailTeams,
|
||||||
CreateGachaClaims,
|
CreateGachaClaims,
|
||||||
ReadDetailGachaClaims,
|
ReadDetailGachaClaims,
|
||||||
ReadListGachaItems,
|
ReadListGachaItems,
|
||||||
@@ -126,6 +128,8 @@ impl PermissionsEnum {
|
|||||||
PermissionsEnum::DeleteGachaRolls => "12345678-ABCD-EFAB-CDEF-0123456789AB",
|
PermissionsEnum::DeleteGachaRolls => "12345678-ABCD-EFAB-CDEF-0123456789AB",
|
||||||
PermissionsEnum::ReadListMentors => "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
|
PermissionsEnum::ReadListMentors => "a1b2c3d4-5e6f-7890-abcd-ef1234567890",
|
||||||
PermissionsEnum::ReadDetailMentors => "b2c3d4e5-6f78-9012-bcde-f23456789012",
|
PermissionsEnum::ReadDetailMentors => "b2c3d4e5-6f78-9012-bcde-f23456789012",
|
||||||
|
PermissionsEnum::ReadListTeams => "e1f2g3h4-5i6j-7k8l-9m0n-op1q2r3s4t5u",
|
||||||
|
PermissionsEnum::ReadDetailTeams => "f2g3h4i5-6j7k-8l9m-0n1o-pq2r3s4t5u6",
|
||||||
PermissionsEnum::RegisterMentors => "c3d4e5f6-7890-1234-cdef-345678901234",
|
PermissionsEnum::RegisterMentors => "c3d4e5f6-7890-1234-cdef-345678901234",
|
||||||
PermissionsEnum::ReadOwnMentorProfile => {
|
PermissionsEnum::ReadOwnMentorProfile => {
|
||||||
"d4e5f6a7-8901-2345-def0-456789012345"
|
"d4e5f6a7-8901-2345-def0-456789012345"
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ use crate::{
|
|||||||
MessageResponseDto, ResponseListSuccessDto, ResponseSuccessDto,
|
MessageResponseDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
TeamsCreateRequestDto, TeamsDetailItemDto, TeamsListItemDto, permissions_guard,
|
TeamsCreateRequestDto, TeamsDetailItemDto, TeamsListItemDto, permissions_guard,
|
||||||
TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto,
|
TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto,
|
||||||
TeamMemberDto, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto
|
TeamMemberDto, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto,
|
||||||
|
AdminTeamsListItemDto, AdminTeamsDetailItemDto, PermissionsEnum
|
||||||
};
|
};
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
@@ -363,4 +364,102 @@ pub async fn get_public_team_by_id(
|
|||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
TeamsService::get_team_by_id(&state, id).await
|
TeamsService::get_team_by_id(&state, id).await
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/teams/admin",
|
||||||
|
params(
|
||||||
|
("page" = Option<i64>, Query, description = "Page number"),
|
||||||
|
("per_page" = Option<i64>, Query, description = "Items per page"),
|
||||||
|
("search" = Option<String>, Query, description = "Search keyword"),
|
||||||
|
("sort_by" = Option<String>, Query, description = "Sort by field"),
|
||||||
|
("order" = Option<String>, Query, description = "Order ASC or DESC"),
|
||||||
|
("filter" = Option<String>, Query, description = "Filter value"),
|
||||||
|
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get admin team list", body = ResponseListSuccessDto<Vec<AdminTeamsListItemDto>>)
|
||||||
|
),
|
||||||
|
tag = "Teams - Admin"
|
||||||
|
)]
|
||||||
|
pub async fn get_admin_team_list(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
axum::extract::Query(meta): axum::extract::Query<MetaRequestDto>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
headers,
|
||||||
|
Extension(state),
|
||||||
|
vec![PermissionsEnum::ReadListTeams.to_string()],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((_claims, state)) => TeamsService::get_admin_team_list(&state, meta).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/teams/admin/{id}",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Team ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get admin team by ID", body = ResponseSuccessDto<AdminTeamsDetailItemDto>)
|
||||||
|
),
|
||||||
|
tag = "Teams - Admin"
|
||||||
|
)]
|
||||||
|
pub async fn get_admin_team_by_id(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
headers,
|
||||||
|
Extension(state),
|
||||||
|
vec![PermissionsEnum::ReadDetailTeams.to_string()],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((_claims, state)) => TeamsService::get_admin_team_by_id(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
get,
|
||||||
|
security(
|
||||||
|
("Bearer" = [])
|
||||||
|
),
|
||||||
|
path = "/v1/teams/admin/{id}/members",
|
||||||
|
params(
|
||||||
|
("id" = String, Path, description = "Team ID")
|
||||||
|
),
|
||||||
|
responses(
|
||||||
|
(status = 200, description = "Get admin team members", body = ResponseSuccessDto<Vec<TeamMemberDto>>)
|
||||||
|
),
|
||||||
|
tag = "Teams - Admin"
|
||||||
|
)]
|
||||||
|
pub async fn get_admin_team_members(
|
||||||
|
headers: HeaderMap,
|
||||||
|
Extension(state): Extension<AppState>,
|
||||||
|
Path(id): Path<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
match permissions_guard(
|
||||||
|
headers,
|
||||||
|
Extension(state),
|
||||||
|
vec![PermissionsEnum::ReadDetailTeams.to_string()],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok((_claims, state)) => TeamsService::get_admin_team_members(&state, id).await,
|
||||||
|
Err(response) => response,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -266,28 +266,163 @@ impl TeamsDetailQueryDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TeamsListQueryDto {
|
impl TeamsListQueryDto {
|
||||||
pub fn from(self) -> TeamsListItemDto {
|
pub fn from(self) -> TeamsListItemDto {
|
||||||
TeamsListItemDto {
|
TeamsListItemDto {
|
||||||
id: self.id.id.to_raw(),
|
id: self.id.id.to_raw(),
|
||||||
name: self.name,
|
name: self.name,
|
||||||
description: self.description,
|
description: self.description,
|
||||||
leader: TeamMemberDto {
|
leader: TeamMemberDto {
|
||||||
id: String::new(),
|
id: String::new(),
|
||||||
user_id: self.leader_id.id.to_raw(),
|
user_id: self.leader_id.id.to_raw(),
|
||||||
fullname: String::new(),
|
fullname: String::new(),
|
||||||
email: None,
|
email: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
role: "leader".to_string(),
|
role: "leader".to_string(),
|
||||||
skills: None,
|
skills: None,
|
||||||
joined_at: self.created_at.clone(),
|
joined_at: self.created_at.clone(),
|
||||||
},
|
},
|
||||||
is_open: self.is_open,
|
is_open: self.is_open,
|
||||||
current_member_count: 1,
|
current_member_count: 1,
|
||||||
max_members: self.max_members,
|
max_members: self.max_members,
|
||||||
skills_required: self.skills_required,
|
skills_required: self.skills_required,
|
||||||
location: self.location,
|
location: self.location,
|
||||||
avatar: self.avatar,
|
avatar: self.avatar,
|
||||||
created_at: self.created_at,
|
created_at: self.created_at,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn to_admin_list_dto(self) -> AdminTeamsListItemDto {
|
||||||
|
let members = vec![];
|
||||||
|
AdminTeamsListItemDto {
|
||||||
|
id: self.id.id.to_raw(),
|
||||||
|
name: self.name,
|
||||||
|
description: self.description,
|
||||||
|
leader: TeamMemberDto {
|
||||||
|
id: String::new(),
|
||||||
|
user_id: self.leader_id.id.to_raw(),
|
||||||
|
fullname: String::new(),
|
||||||
|
email: None,
|
||||||
|
avatar: None,
|
||||||
|
role: "leader".to_string(),
|
||||||
|
skills: None,
|
||||||
|
joined_at: self.created_at.clone(),
|
||||||
|
},
|
||||||
|
is_open: self.is_open,
|
||||||
|
current_member_count: members.len() as i32 + 1,
|
||||||
|
max_members: self.max_members,
|
||||||
|
skills_required: self.skills_required,
|
||||||
|
location: self.location,
|
||||||
|
avatar: self.avatar,
|
||||||
|
website_url: None,
|
||||||
|
github_url: None,
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: self.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AdminTeamsListItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub leader: TeamMemberDto,
|
||||||
|
pub is_open: bool,
|
||||||
|
pub current_member_count: i32,
|
||||||
|
pub max_members: Option<i32>,
|
||||||
|
pub skills_required: Option<Vec<String>>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub website_url: Option<String>,
|
||||||
|
pub github_url: Option<String>,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||||
|
pub struct AdminTeamsDetailItemDto {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub leader: TeamMemberDto,
|
||||||
|
pub is_open: bool,
|
||||||
|
pub max_members: Option<i32>,
|
||||||
|
pub current_member_count: i32,
|
||||||
|
pub skills_required: Option<Vec<String>>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub avatar: Option<String>,
|
||||||
|
pub website_url: Option<String>,
|
||||||
|
pub github_url: Option<String>,
|
||||||
|
pub members: Vec<TeamMemberDto>,
|
||||||
|
pub is_active: bool,
|
||||||
|
pub is_deleted: bool,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeamsListQueryDto {
|
||||||
|
pub fn to_admin_list_dto(self) -> AdminTeamsListItemDto {
|
||||||
|
let members = vec![];
|
||||||
|
AdminTeamsListItemDto {
|
||||||
|
id: self.id.id.to_raw(),
|
||||||
|
name: self.name,
|
||||||
|
description: self.description,
|
||||||
|
leader: TeamMemberDto {
|
||||||
|
id: String::new(),
|
||||||
|
user_id: self.leader_id.id.to_raw(),
|
||||||
|
fullname: String::new(),
|
||||||
|
email: None,
|
||||||
|
avatar: None,
|
||||||
|
role: "leader".to_string(),
|
||||||
|
skills: None,
|
||||||
|
joined_at: self.created_at.clone(),
|
||||||
|
},
|
||||||
|
is_open: self.is_open,
|
||||||
|
current_member_count: members.len() as i32 + 1,
|
||||||
|
max_members: self.max_members,
|
||||||
|
skills_required: self.skills_required,
|
||||||
|
location: self.location,
|
||||||
|
avatar: self.avatar,
|
||||||
|
website_url: None,
|
||||||
|
github_url: None,
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: self.created_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TeamsDetailQueryDto {
|
||||||
|
pub fn to_admin_detail_dto(self, members: Vec<TeamMemberDto>) -> AdminTeamsDetailItemDto {
|
||||||
|
AdminTeamsDetailItemDto {
|
||||||
|
id: self.id.id.to_raw(),
|
||||||
|
name: self.name,
|
||||||
|
description: self.description,
|
||||||
|
leader: TeamMemberDto {
|
||||||
|
id: String::new(),
|
||||||
|
user_id: self.leader_id.id.to_raw(),
|
||||||
|
fullname: String::new(),
|
||||||
|
email: None,
|
||||||
|
avatar: None,
|
||||||
|
role: "leader".to_string(),
|
||||||
|
skills: None,
|
||||||
|
joined_at: self.created_at.clone(),
|
||||||
|
},
|
||||||
|
is_open: self.is_open,
|
||||||
|
current_member_count: members.len() as i32 + 1,
|
||||||
|
max_members: self.max_members,
|
||||||
|
skills_required: self.skills_required,
|
||||||
|
location: self.location,
|
||||||
|
avatar: self.avatar,
|
||||||
|
website_url: self.website_url,
|
||||||
|
github_url: self.github_url,
|
||||||
|
members,
|
||||||
|
is_active: self.is_active,
|
||||||
|
is_deleted: self.is_deleted,
|
||||||
|
created_at: self.created_at,
|
||||||
|
updated_at: self.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@ use super::{
|
|||||||
TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto,
|
TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto,
|
||||||
TeamAcceptInvitationRequestDto, TeamsDetailItemDto,
|
TeamAcceptInvitationRequestDto, TeamsDetailItemDto,
|
||||||
TeamMemberDto, TeamsRepository, TeamsSchema, TeamMembersSchema,
|
TeamMemberDto, TeamsRepository, TeamsSchema, TeamMembersSchema,
|
||||||
TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto
|
TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto,
|
||||||
|
AdminTeamsListItemDto, AdminTeamsDetailItemDto
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||||
@@ -33,6 +34,9 @@ pub trait TeamsServiceTrait: Send + Sync + 'static {
|
|||||||
fn get_team_members(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
fn get_team_members(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
fn leave_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
fn leave_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
fn search_teams(state: &AppState, search_params: TeamsSearchQueryDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
fn search_teams(state: &AppState, search_params: TeamsSearchQueryDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn get_admin_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn get_admin_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
|
fn get_admin_team_members(state: &AppState, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -654,6 +658,109 @@ impl TeamsServiceTrait for TeamsService {
|
|||||||
};
|
};
|
||||||
success_list_response(response)
|
success_list_response(response)
|
||||||
}
|
}
|
||||||
|
fn get_admin_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
let repo = TeamsRepository::new(&state);
|
||||||
|
match repo.query_team_list(meta).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let response = ResponseListSuccessDto {
|
||||||
|
data: data.data.into_iter().map(|team| team.to_admin_list_dto()).collect(),
|
||||||
|
meta: data.meta,
|
||||||
|
};
|
||||||
|
success_list_response(response)
|
||||||
|
}
|
||||||
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_admin_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
if Uuid::parse_str(&id).is_err() {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
|
||||||
|
}
|
||||||
|
let repo = TeamsRepository::new(&state);
|
||||||
|
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id);
|
||||||
|
match repo.query_team_by_id(&thing_id).await {
|
||||||
|
Ok(team) if !team.is_deleted => {
|
||||||
|
let members = repo.query_team_members(&team.id).await.unwrap_or_default();
|
||||||
|
let mut member_dtos = Vec::new();
|
||||||
|
|
||||||
|
for member in members {
|
||||||
|
match Self::get_user_info_with_privacy(
|
||||||
|
&member.user_id.id.to_raw(),
|
||||||
|
"system", // Admin context - show all sensitive data
|
||||||
|
true, // Admin context - always show sensitive data
|
||||||
|
&state,
|
||||||
|
).await {
|
||||||
|
Ok(mut member_dto) => {
|
||||||
|
member_dto.role = member.role;
|
||||||
|
member_dto.joined_at = member.joined_at;
|
||||||
|
member_dtos.push(member_dto);
|
||||||
|
}
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add leader with full sensitive info
|
||||||
|
match Self::get_user_info_with_privacy(
|
||||||
|
&team.leader_id.id.to_raw(),
|
||||||
|
"system",
|
||||||
|
true,
|
||||||
|
&state,
|
||||||
|
).await {
|
||||||
|
Ok(mut leader_dto) => {
|
||||||
|
leader_dto.role = "leader".to_string();
|
||||||
|
member_dtos.insert(0, leader_dto);
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let team_dto = team.to_admin_detail_dto(member_dtos);
|
||||||
|
success_response(ResponseSuccessDto { data: team_dto })
|
||||||
|
}
|
||||||
|
Ok(_) => common_response(StatusCode::NOT_FOUND, "Team not found"),
|
||||||
|
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_admin_team_members(state: &AppState, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||||
|
let state = state.to_owned();
|
||||||
|
Box::pin(async move {
|
||||||
|
if Uuid::parse_str(&team_id).is_err() {
|
||||||
|
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
|
||||||
|
}
|
||||||
|
let repo = TeamsRepository::new(&state);
|
||||||
|
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &team_id);
|
||||||
|
|
||||||
|
let members = match repo.query_team_members(&thing_id).await {
|
||||||
|
Ok(members) => members,
|
||||||
|
Err(e) => return common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut member_dtos = Vec::new();
|
||||||
|
for member in members {
|
||||||
|
match Self::get_user_info_with_privacy(
|
||||||
|
&member.user_id.id.to_raw(),
|
||||||
|
"system", // Admin context - show all sensitive data
|
||||||
|
true, // Admin context - always show sensitive data
|
||||||
|
&state,
|
||||||
|
).await {
|
||||||
|
Ok(mut member_dto) => {
|
||||||
|
member_dto.role = member.role;
|
||||||
|
member_dto.joined_at = member.joined_at;
|
||||||
|
member_dtos.push(member_dto);
|
||||||
|
}
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
success_response(ResponseSuccessDto { data: member_dtos })
|
||||||
|
})
|
||||||
|
}
|
||||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,326 @@
|
|||||||
|
use crate::get_app_state;
|
||||||
|
use axum::{http::HeaderMap, response::Response};
|
||||||
|
use imphnen_iam::{
|
||||||
|
AppState, Claims, PermissionsEnum, ResponseSuccessDto, ResponseListSuccessDto,
|
||||||
|
AdminTeamsListItemDto, AdminTeamsDetailItemDto, TeamMemberDto
|
||||||
|
};
|
||||||
|
use imphnen_libs::jsonwebtoken::{encode, Header};
|
||||||
|
use imphnen_utils::make_thing_from_enum;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use uuid::Uuid;
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_admin_team_endpoints_sensitive_data_exposure() {
|
||||||
|
let app_state = get_app_state().await;
|
||||||
|
let repo = imphnen_iam::TeamsRepository::new(&app_state);
|
||||||
|
|
||||||
|
// Create test data
|
||||||
|
let team_id = Uuid::new_v4().to_string();
|
||||||
|
let leader_id = Uuid::new_v4().to_string();
|
||||||
|
let member_id_1 = Uuid::new_v4().to_string();
|
||||||
|
let member_id_2 = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
// Create test team
|
||||||
|
let team = imphnen_iam::TeamsSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &team_id),
|
||||||
|
name: "Admin Test Team".to_string(),
|
||||||
|
description: Some("Test team for admin endpoints".to_string()),
|
||||||
|
leader_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &leader_id),
|
||||||
|
is_open: true,
|
||||||
|
max_members: Some(10),
|
||||||
|
skills_required: Some(vec!["Rust".to_string(), "Backend".to_string()]),
|
||||||
|
location: Some("Remote".to_string()),
|
||||||
|
avatar: Some("https://example.com/avatar.jpg".to_string()),
|
||||||
|
website_url: Some("https://example.com".to_string()),
|
||||||
|
github_url: Some("https://github.com/example".to_string()),
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Utc::now().to_rfc3339(),
|
||||||
|
updated_at: Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let create_result = repo.query_create_team(team.clone()).await;
|
||||||
|
assert!(create_result.is_ok(), "Failed to create test team");
|
||||||
|
|
||||||
|
// Create test members
|
||||||
|
let member_1 = imphnen_iam::TeamMembersSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::TeamMembers, &Uuid::new_v4().to_string()),
|
||||||
|
team_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &team_id),
|
||||||
|
user_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &member_id_1),
|
||||||
|
role: "member".to_string(),
|
||||||
|
joined_at: Utc::now().to_rfc3339(),
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let member_2 = imphnen_iam::TeamMembersSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::TeamMembers, &Uuid::new_v4().to_string()),
|
||||||
|
team_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &team_id),
|
||||||
|
user_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &member_id_2),
|
||||||
|
role: "contributor".to_string(),
|
||||||
|
joined_at: Utc::now().to_rfc3339(),
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let add_member_result_1 = repo.query_add_team_member(member_1).await;
|
||||||
|
let add_member_result_2 = repo.query_add_team_member(member_2).await;
|
||||||
|
|
||||||
|
assert!(add_member_result_1.is_ok(), "Failed to add test member 1");
|
||||||
|
assert!(add_member_result_2.is_ok(), "Failed to add test member 2");
|
||||||
|
|
||||||
|
// Create admin user with proper permissions
|
||||||
|
let admin_claims = Claims {
|
||||||
|
user_id: "admin_user_123".to_string(),
|
||||||
|
email: "admin@example.com".to_string(),
|
||||||
|
fullname: "Admin User".to_string(),
|
||||||
|
avatar: None,
|
||||||
|
role: imphnen_iam::RoleSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Roles, "admin_role"),
|
||||||
|
name: "Admin".to_string(),
|
||||||
|
permissions: vec![
|
||||||
|
imphnen_iam::PermissionSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Permissions, "read_list_teams"),
|
||||||
|
name: PermissionsEnum::ReadListTeams.to_string(),
|
||||||
|
},
|
||||||
|
imphnen_iam::PermissionSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Permissions, "read_detail_teams"),
|
||||||
|
name: PermissionsEnum::ReadDetailTeams.to_string(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
exp: 1_000_000_000,
|
||||||
|
iat: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let admin_token = encode(&Header::default(), &admin_claims, &app_state.jwt_secret).unwrap();
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Authorization", format!("Bearer {}", admin_token).parse().unwrap());
|
||||||
|
|
||||||
|
// Test 1: Admin team list endpoint should expose sensitive fields
|
||||||
|
let response = imphnen_iam::teams_controller::get_admin_team_list(
|
||||||
|
headers.clone(),
|
||||||
|
axum::extract::Extension(app_state.clone()),
|
||||||
|
axum::extract::Query(imphnen_iam::MetaRequestDto {
|
||||||
|
page: Some(1),
|
||||||
|
per_page: Some(10),
|
||||||
|
search: None,
|
||||||
|
sort_by: None,
|
||||||
|
order: None,
|
||||||
|
filter: None,
|
||||||
|
filter_by: None,
|
||||||
|
}),
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert!(response.status().is_success(), "Admin team list should return success");
|
||||||
|
|
||||||
|
let response_body = match response.into_body().into_string().await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(e) => panic!("Failed to read response body: {}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_json: ResponseListSuccessDto<Vec<AdminTeamsListItemDto>> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
|
||||||
|
// Verify sensitive fields are present in admin response
|
||||||
|
assert!(response_json.data.iter().any(|team| {
|
||||||
|
team.is_deleted == false && // Should show is_deleted field
|
||||||
|
team.is_active == true && // Should show is_active field
|
||||||
|
team.website_url.is_some() && // Should show website_url
|
||||||
|
team.github_url.is_some() // Should show github_url
|
||||||
|
}), "Admin team list should expose sensitive fields");
|
||||||
|
|
||||||
|
// Test 2: Admin team detail endpoint should expose sensitive fields and full member info
|
||||||
|
let response = imphnen_iam::teams_controller::get_admin_team_by_id(
|
||||||
|
headers.clone(),
|
||||||
|
axum::extract::Extension(app_state.clone()),
|
||||||
|
axum::extract::Path(team_id.clone()),
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert!(response.status().is_success(), "Admin team detail should return success");
|
||||||
|
|
||||||
|
let response_body = match response.into_body().into_string().await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(e) => panic!("Failed to read response body: {}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_json: ResponseSuccessDto<AdminTeamsDetailItemDto> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
|
||||||
|
let admin_team = response_json.data;
|
||||||
|
|
||||||
|
// Verify sensitive fields are present
|
||||||
|
assert!(admin_team.is_deleted == false, "Admin team detail should show is_deleted field");
|
||||||
|
assert!(admin_team.is_active == true, "Admin team detail should show is_active field");
|
||||||
|
assert!(admin_team.website_url.is_some(), "Admin team detail should show website_url");
|
||||||
|
assert!(admin_team.github_url.is_some(), "Admin team detail should show github_url");
|
||||||
|
assert!(admin_team.members.len() >= 2, "Admin team detail should show all members");
|
||||||
|
|
||||||
|
// Verify all members have sensitive info (email should be present for admins)
|
||||||
|
let has_all_member_info = admin_team.members.iter().all(|member| {
|
||||||
|
member.email.is_some() && // Admin should see member emails
|
||||||
|
member.fullname != "" && // Admin should see fullnames
|
||||||
|
member.role != "" // Admin should see roles
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(has_all_member_info, "Admin team detail should expose all member sensitive information");
|
||||||
|
|
||||||
|
// Test 3: Admin team members endpoint should expose sensitive info
|
||||||
|
let response = imphnen_iam::teams_controller::get_admin_team_members(
|
||||||
|
headers,
|
||||||
|
axum::extract::Extension(app_state),
|
||||||
|
axum::extract::Path(team_id),
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert!(response.status().is_success(), "Admin team members should return success");
|
||||||
|
|
||||||
|
let response_body = match response.into_body().into_string().await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(e) => panic!("Failed to read response body: {}", e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response_json: ResponseSuccessDto<Vec<TeamMemberDto>> =
|
||||||
|
serde_json::from_str(&response_body).unwrap();
|
||||||
|
|
||||||
|
let admin_members = response_json.data;
|
||||||
|
|
||||||
|
// Verify all members have sensitive info
|
||||||
|
let has_all_member_info = admin_members.iter().all(|member| {
|
||||||
|
member.email.is_some() && // Admin should see member emails
|
||||||
|
member.fullname != "" && // Admin should see fullnames
|
||||||
|
member.role != "" // Admin should see roles
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(has_all_member_info, "Admin team members endpoint should expose all member sensitive information");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = repo.query_delete_team(team_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_admin_team_endpoints_permission_guard() {
|
||||||
|
let app_state = get_app_state().await;
|
||||||
|
|
||||||
|
// Create test team first
|
||||||
|
let team_id = Uuid::new_v4().to_string();
|
||||||
|
let leader_id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
let team = imphnen_iam::TeamsSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &team_id),
|
||||||
|
name: "Permission Test Team".to_string(),
|
||||||
|
description: Some("Test team for permission checks".to_string()),
|
||||||
|
leader_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &leader_id),
|
||||||
|
is_open: true,
|
||||||
|
max_members: Some(10),
|
||||||
|
skills_required: Some(vec!["Rust".to_string()]),
|
||||||
|
location: Some("Remote".to_string()),
|
||||||
|
avatar: None,
|
||||||
|
website_url: None,
|
||||||
|
github_url: None,
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Utc::now().to_rfc3339(),
|
||||||
|
updated_at: Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let repo = imphnen_iam::TeamsRepository::new(&app_state);
|
||||||
|
let create_result = repo.query_create_team(team).await;
|
||||||
|
assert!(create_result.is_ok(), "Failed to create test team");
|
||||||
|
|
||||||
|
// Create regular user without admin permissions
|
||||||
|
let regular_claims = Claims {
|
||||||
|
user_id: "regular_user_123".to_string(),
|
||||||
|
email: "user@example.com".to_string(),
|
||||||
|
fullname: "Regular User".to_string(),
|
||||||
|
avatar: None,
|
||||||
|
role: imphnen_iam::RoleSchema {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Roles, "user_role"),
|
||||||
|
name: "User".to_string(),
|
||||||
|
permissions: vec![], // No admin permissions
|
||||||
|
},
|
||||||
|
exp: 1_000_000_000,
|
||||||
|
iat: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let regular_token = encode(&Header::default(), ®ular_claims, &app_state.jwt_secret).unwrap();
|
||||||
|
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
headers.insert("Authorization", format!("Bearer {}", regular_token).parse().unwrap());
|
||||||
|
|
||||||
|
// Test that regular user gets forbidden for admin endpoints
|
||||||
|
let response = imphnen_iam::teams_controller::get_admin_team_list(
|
||||||
|
headers,
|
||||||
|
axum::extract::Extension(app_state),
|
||||||
|
axum::extract::Query(imphnen_iam::MetaRequestDto {
|
||||||
|
page: Some(1),
|
||||||
|
per_page: Some(10),
|
||||||
|
search: None,
|
||||||
|
sort_by: None,
|
||||||
|
order: None,
|
||||||
|
filter: None,
|
||||||
|
filter_by: None,
|
||||||
|
}),
|
||||||
|
).await;
|
||||||
|
|
||||||
|
assert_eq!(response.status().as_u16(), 403, "Regular user should get forbidden for admin endpoints");
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = repo.query_delete_team(team_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_admin_team_dto_conversion_edge_cases() {
|
||||||
|
// Test DTO conversion with empty member list
|
||||||
|
let team_query_dto = imphnen_iam::TeamsDetailQueryDto {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &Uuid::new_v4().to_string()),
|
||||||
|
name: "Test Team".to_string(),
|
||||||
|
description: Some("Test description".to_string()),
|
||||||
|
leader_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &Uuid::new_v4().to_string()),
|
||||||
|
is_open: true,
|
||||||
|
max_members: Some(10),
|
||||||
|
skills_required: Some(vec!["Rust".to_string()]),
|
||||||
|
location: Some("Remote".to_string()),
|
||||||
|
avatar: Some("https://example.com/avatar.jpg".to_string()),
|
||||||
|
website_url: Some("https://example.com".to_string()),
|
||||||
|
github_url: Some("https://github.com/example".to_string()),
|
||||||
|
is_active: true,
|
||||||
|
is_deleted: false,
|
||||||
|
created_at: Utc::now().to_rfc3339(),
|
||||||
|
updated_at: Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let admin_dto = team_query_dto.to_admin_detail_dto(vec![]); // Empty member list
|
||||||
|
|
||||||
|
// Should handle empty member list gracefully
|
||||||
|
assert_eq!(admin_dto.members.len(), 0, "Should handle empty member list");
|
||||||
|
assert_eq!(admin_dto.current_member_count, 1, "Current member count should be 1 (leader only)");
|
||||||
|
assert_eq!(admin_dto.is_deleted, false, "Should preserve is_deleted field");
|
||||||
|
assert_eq!(admin_dto.is_active, true, "Should preserve is_active field");
|
||||||
|
assert!(admin_dto.website_url.is_some(), "Should preserve website_url");
|
||||||
|
assert!(admin_dto.github_url.is_some(), "Should preserve github_url");
|
||||||
|
|
||||||
|
// Test DTO conversion with deleted team
|
||||||
|
let deleted_team_query_dto = imphnen_iam::TeamsDetailQueryDto {
|
||||||
|
id: make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &Uuid::new_v4().to_string()),
|
||||||
|
name: "Deleted Team".to_string(),
|
||||||
|
description: Some("This team is deleted".to_string()),
|
||||||
|
leader_id: make_thing_from_enum(imphnen_libs::ResourceEnum::Users, &Uuid::new_v4().to_string()),
|
||||||
|
is_open: true,
|
||||||
|
max_members: Some(10),
|
||||||
|
skills_required: Some(vec!["Rust".to_string()]),
|
||||||
|
location: Some("Remote".to_string()),
|
||||||
|
avatar: Some("https://example.com/avatar.jpg".to_string()),
|
||||||
|
website_url: Some("https://example.com".to_string()),
|
||||||
|
github_url: Some("https://github.com/example".to_string()),
|
||||||
|
is_active: false,
|
||||||
|
is_deleted: true, // Mark as deleted
|
||||||
|
created_at: Utc::now().to_rfc3339(),
|
||||||
|
updated_at: Utc::now().to_rfc3339(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let deleted_admin_dto = deleted_team_query_dto.to_admin_detail_dto(vec![]);
|
||||||
|
|
||||||
|
assert_eq!(deleted_admin_dto.is_deleted, true, "Should preserve is_deleted field for deleted teams");
|
||||||
|
assert_eq!(deleted_admin_dto.is_active, false, "Should preserve is_active field for deleted teams");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user