feat: Add member team management functionality with detailed DTOs and service methods
This commit is contained in:
@@ -6,11 +6,13 @@ use crate::{
|
||||
TeamMemberDto, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto,
|
||||
AdminTeamsListItemDto, AdminTeamsDetailItemDto, PermissionsEnum
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
use axum::extract::Query;
|
||||
use axum::extract::Path;
|
||||
use axum::http::HeaderMap;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{Extension, Json};
|
||||
|
||||
use super::teams_service::{TeamsServiceTrait, TeamsService};
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -29,55 +31,60 @@ use super::teams_service::{TeamsServiceTrait, TeamsService};
|
||||
("filter_by" = Option<String>, Query, description = "Field to filter by"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get team list", body = ResponseListSuccessDto<Vec<TeamsListItemDto>>)
|
||||
(status = 200, description = "Get team list", body = ResponseListSuccessDto<Vec<TeamsListItemDto>>),
|
||||
(status = 200, description = "Get public team list", body = ResponseListSuccessDto<Vec<PublicTeamsListItemDto>>)
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn get_team_list(
|
||||
headers: HeaderMap,
|
||||
headers: Option<HeaderMap>,
|
||||
Extension(state): Extension<AppState>,
|
||||
axum::extract::Query(meta): axum::extract::Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_claims, state)) => TeamsService::get_team_list(&state, meta).await,
|
||||
Err(response) => response,
|
||||
match headers {
|
||||
Some(headers) => {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state.clone()),
|
||||
vec![],
|
||||
).await {
|
||||
Ok((_claims, state)) => TeamsService::get_team_list(&state, meta).await,
|
||||
Err(_) => TeamsService::get_public_team_list(&state, meta).await,
|
||||
}
|
||||
},
|
||||
None => TeamsService::get_public_team_list(&state, meta).await,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/detail/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get team by ID", body = ResponseSuccessDto<TeamsDetailItemDto>)
|
||||
),
|
||||
tag = "Teams"
|
||||
get,
|
||||
path = "/v1/teams/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get team by ID", body = ResponseSuccessDto<TeamsDetailItemDto>),
|
||||
(status = 200, description = "Get public team by ID", body = ResponseSuccessDto<PublicTeamsDetailItemDto>)
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn get_team_by_id(
|
||||
headers: HeaderMap,
|
||||
headers: Option<HeaderMap>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((_claims, state)) => TeamsService::get_team_by_id(&state, id).await,
|
||||
Err(response) => response,
|
||||
match headers {
|
||||
Some(headers) => {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state.clone()),
|
||||
vec![],
|
||||
).await {
|
||||
Ok((_claims, state)) => TeamsService::get_team_by_id(&state, id).await,
|
||||
Err(_) => TeamsService::get_public_team_by_id(&state, id).await,
|
||||
}
|
||||
},
|
||||
None => TeamsService::get_public_team_by_id(&state, id).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +310,7 @@ pub async fn get_team_members(
|
||||
("id" = String, Path, description = "Team ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Leave team", body = MessageResponseDto)
|
||||
(status = 200, description = "Leave specific team", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
@@ -325,45 +332,32 @@ pub async fn post_leave_team(
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/teams/public",
|
||||
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"),
|
||||
),
|
||||
post,
|
||||
security(
|
||||
("Bearer" = [])
|
||||
),
|
||||
path = "/v1/teams/leave-me",
|
||||
responses(
|
||||
(status = 200, description = "Get public team list", body = ResponseListSuccessDto<Vec<PublicTeamsListItemDto>>)
|
||||
(status = 200, description = "Leave current team", body = MessageResponseDto)
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn get_public_team_list(
|
||||
pub async fn post_leave_current_team(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
axum::extract::Query(meta): axum::extract::Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
TeamsService::get_team_list(&state, meta).await
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state),
|
||||
vec![],
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((claims, state)) => TeamsService::leave_current_team(&state, claims).await,
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/v1/teams/public/{id}",
|
||||
params(
|
||||
("id" = String, Path, description = "Team ID")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Get public team by ID", body = ResponseSuccessDto<PublicTeamsDetailItemDto>)
|
||||
),
|
||||
tag = "Teams"
|
||||
)]
|
||||
pub async fn get_public_team_by_id(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
TeamsService::get_team_by_id(&state, id).await
|
||||
#[utoipa::path(
|
||||
get,
|
||||
security(
|
||||
|
||||
@@ -110,6 +110,26 @@ pub struct TeamsDetailItemDto {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct MemberTeamsDetailItemDto {
|
||||
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>, // Always include members for authenticated users
|
||||
pub is_active: bool,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct TeamsListItemDto {
|
||||
pub id: String,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto,
|
||||
TeamAcceptInvitationRequestDto, TeamsDetailItemDto,
|
||||
TeamAcceptInvitationRequestDto, TeamsDetailItemDto, MemberTeamsDetailItemDto,
|
||||
TeamMemberDto, TeamsRepository, TeamsSchema, TeamMembersSchema,
|
||||
TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto,
|
||||
AdminTeamsListItemDto, AdminTeamsDetailItemDto
|
||||
@@ -24,6 +24,8 @@ use chrono::Utc;
|
||||
pub trait TeamsServiceTrait: Send + Sync + 'static {
|
||||
fn get_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_member_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_member_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_public_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn get_public_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
fn create_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, new_team: TeamsCreateRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
||||
@@ -33,6 +35,7 @@ pub trait TeamsServiceTrait: Send + Sync + 'static {
|
||||
fn accept_invitation(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, accept: TeamAcceptInvitationRequestDto) -> 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_current_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims) -> 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>>;
|
||||
@@ -107,69 +110,167 @@ impl TeamsService {
|
||||
|
||||
impl TeamsServiceTrait for TeamsService {
|
||||
fn get_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,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
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,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_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();
|
||||
|
||||
// For public team details, only show sensitive info if user is authenticated and part of the team
|
||||
let team_dto = TeamsDetailItemDto {
|
||||
id: team.id.id.to_raw(),
|
||||
name: team.name,
|
||||
description: team.description,
|
||||
leader: TeamMemberDto {
|
||||
id: String::new(),
|
||||
user_id: team.leader_id.id.to_raw(),
|
||||
fullname: String::new(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
role: "leader".to_string(),
|
||||
skills: None,
|
||||
joined_at: team.created_at.clone(),
|
||||
},
|
||||
is_open: team.is_open,
|
||||
max_members: team.max_members,
|
||||
current_member_count: members.len() as i32 + 1,
|
||||
skills_required: team.skills_required,
|
||||
location: team.location,
|
||||
avatar: team.avatar,
|
||||
website_url: team.website_url,
|
||||
github_url: team.github_url,
|
||||
members: None,
|
||||
is_active: team.is_active,
|
||||
created_at: team.created_at,
|
||||
updated_at: team.updated_at,
|
||||
};
|
||||
success_response(ResponseSuccessDto { data: team_dto })
|
||||
})
|
||||
}
|
||||
|
||||
fn get_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");
|
||||
}
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Team not found"),
|
||||
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
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();
|
||||
|
||||
// For public team details, only show sensitive info if user is authenticated and part of the team
|
||||
let team_dto = TeamsDetailItemDto {
|
||||
id: team.id.id.to_raw(),
|
||||
name: team.name,
|
||||
description: team.description,
|
||||
leader: TeamMemberDto {
|
||||
id: String::new(),
|
||||
user_id: team.leader_id.id.to_raw(),
|
||||
fullname: String::new(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
role: "leader".to_string(),
|
||||
skills: None,
|
||||
joined_at: team.created_at.clone(),
|
||||
},
|
||||
is_open: team.is_open,
|
||||
max_members: team.max_members,
|
||||
current_member_count: members.len() as i32 + 1,
|
||||
skills_required: team.skills_required,
|
||||
location: team.location,
|
||||
avatar: team.avatar,
|
||||
website_url: team.website_url,
|
||||
github_url: team.github_url,
|
||||
members: None,
|
||||
is_active: team.is_active,
|
||||
created_at: team.created_at,
|
||||
updated_at: team.updated_at,
|
||||
};
|
||||
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_member_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,
|
||||
meta: data.meta,
|
||||
};
|
||||
success_list_response(response)
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn get_member_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();
|
||||
|
||||
// For member team details, include all information including members list
|
||||
let mut member_dtos = Vec::new();
|
||||
for member in members {
|
||||
match Self::get_user_info_with_privacy(
|
||||
&member.user_id.id.to_raw(),
|
||||
"system", // In member context, we show all user info
|
||||
true, // In member context, we show all user info
|
||||
&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 info
|
||||
let leader_dto = 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();
|
||||
leader_dto
|
||||
}
|
||||
Err(_) => TeamMemberDto {
|
||||
id: String::new(),
|
||||
user_id: team.leader_id.id.to_raw(),
|
||||
fullname: String::new(),
|
||||
email: None,
|
||||
avatar: None,
|
||||
role: "leader".to_string(),
|
||||
skills: None,
|
||||
joined_at: team.created_at.clone(),
|
||||
}
|
||||
};
|
||||
|
||||
member_dtos.insert(0, leader_dto);
|
||||
|
||||
let team_dto = MemberTeamsDetailItemDto {
|
||||
id: team.id.id.to_raw(),
|
||||
name: team.name,
|
||||
description: team.description,
|
||||
leader: leader_dto,
|
||||
is_open: team.is_open,
|
||||
max_members: team.max_members,
|
||||
current_member_count: members.len() as i32 + 1,
|
||||
skills_required: team.skills_required,
|
||||
location: team.location,
|
||||
avatar: team.avatar,
|
||||
website_url: team.website_url,
|
||||
github_url: team.github_url,
|
||||
members: member_dtos,
|
||||
is_active: team.is_active,
|
||||
created_at: team.created_at,
|
||||
updated_at: team.updated_at,
|
||||
};
|
||||
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_public_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
let state = state.to_owned();
|
||||
@@ -761,8 +862,44 @@ impl TeamsServiceTrait for TeamsService {
|
||||
success_response(ResponseSuccessDto { data: member_dtos })
|
||||
})
|
||||
}
|
||||
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
||||
|
||||
fn leave_current_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
let repo = TeamsRepository::new(&state);
|
||||
let user_thing = make_thing_from_enum(ResourceEnum::Users, &claims.user_id);
|
||||
|
||||
// Find the teams that the user is a member of
|
||||
let teams = match repo.query_teams_by_user(&user_thing).await {
|
||||
Ok(teams) => teams,
|
||||
Err(e) => {
|
||||
error!("Failed to query user teams: {}", e);
|
||||
return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to retrieve user teams")
|
||||
},
|
||||
};
|
||||
|
||||
if teams.is_empty() {
|
||||
return common_response(StatusCode::BAD_REQUEST, "User is not a member of any team");
|
||||
}
|
||||
|
||||
// For now, assume user is in only one team (common case)
|
||||
// In a future enhancement, we could ask the user to specify which team to leave
|
||||
let team = &teams[0];
|
||||
let team_id = team.id.id.to_raw();
|
||||
let team_thing = make_thing_from_enum(ResourceEnum::Teams, &team_id);
|
||||
|
||||
// Check if user is the leader
|
||||
if team.leader_id.id.to_raw() == claims.user_id {
|
||||
return common_response(StatusCode::FORBIDDEN, "Team leader cannot leave the team");
|
||||
}
|
||||
|
||||
match repo.query_remove_team_member(&team_thing, &user_thing).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &format!("Successfully left team: {}", team.name)),
|
||||
Err(e) => {
|
||||
error!("Failed to remove team member: {}", e);
|
||||
return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to leave team")
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user