This commit is contained in:
MythEclipse
2025-09-25 17:29:17 +07:00
parent df11aa5eb8
commit 82b85abe8f
2 changed files with 481 additions and 509 deletions
+211 -244
View File
@@ -5,129 +5,129 @@ use validator::Validate;
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TeamsCreateRequestDto { pub struct TeamsCreateRequestDto {
#[validate(length(min = 3, max = 100, message = "Team name must be between 3 and 100 characters"))] #[validate(length(min = 3, max = 100, message = "Team name must be between 3 and 100 characters"))]
pub name: String, pub name: String,
#[validate(length(max = 500, message = "Description cannot exceed 500 characters"))] #[validate(length(max = 500, message = "Description cannot exceed 500 characters"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub is_open: Option<bool>, pub is_open: Option<bool>,
#[validate(range(min = 2, max = 50, message = "Max members must be between 2 and 50"))] #[validate(range(min = 2, max = 50, message = "Max members must be between 2 and 50"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max_members: Option<i32>, pub max_members: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub skills_required: Option<Vec<String>>, pub skills_required: Option<Vec<String>>,
#[validate(length(max = 100, message = "Location cannot exceed 100 characters"))] #[validate(length(max = 100, message = "Location cannot exceed 100 characters"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>, pub location: Option<String>,
#[validate(url(message = "Invalid avatar URL"))] #[validate(url(message = "Invalid avatar URL"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>, pub avatar: Option<String>,
#[validate(url(message = "Invalid website URL"))] #[validate(url(message = "Invalid website URL"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub website_url: Option<String>, pub website_url: Option<String>,
#[validate(url(message = "Invalid GitHub URL"))] #[validate(url(message = "Invalid GitHub URL"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub github_url: Option<String>, pub github_url: Option<String>,
#[validate(length(min = 1, message = "Member emails cannot be empty"))] #[validate(length(min = 1, message = "Member emails cannot be empty"))]
pub member_emails: Vec<String>, pub member_emails: Vec<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TeamsUpdateRequestDto { pub struct TeamsUpdateRequestDto {
#[validate(length(min = 3, max = 100, message = "Team name must be between 3 and 100 characters"))] #[validate(length(min = 3, max = 100, message = "Team name must be between 3 and 100 characters"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>, pub name: Option<String>,
#[validate(length(max = 500, message = "Description cannot exceed 500 characters"))] #[validate(length(max = 500, message = "Description cannot exceed 500 characters"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>, pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub is_open: Option<bool>, pub is_open: Option<bool>,
#[validate(range(min = 2, max = 50, message = "Max members must be between 2 and 50"))] #[validate(range(min = 2, max = 50, message = "Max members must be between 2 and 50"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max_members: Option<i32>, pub max_members: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub skills_required: Option<Vec<String>>, pub skills_required: Option<Vec<String>>,
#[validate(length(max = 100, message = "Location cannot exceed 100 characters"))] #[validate(length(max = 100, message = "Location cannot exceed 100 characters"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>, pub location: Option<String>,
#[validate(url(message = "Invalid avatar URL"))] #[validate(url(message = "Invalid avatar URL"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub avatar: Option<String>, pub avatar: Option<String>,
#[validate(url(message = "Invalid website URL"))] #[validate(url(message = "Invalid website URL"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub website_url: Option<String>, pub website_url: Option<String>,
#[validate(url(message = "Invalid GitHub URL"))] #[validate(url(message = "Invalid GitHub URL"))]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub github_url: Option<String>, pub github_url: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct TeamInviteRequestDto { pub struct TeamInviteRequestDto {
#[validate(length(min = 1, message = "Member emails cannot be empty"))] #[validate(length(min = 1, message = "Member emails cannot be empty"))]
pub member_emails: Vec<String>, pub member_emails: Vec<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct TeamAcceptInvitationRequestDto { pub struct TeamAcceptInvitationRequestDto {
pub token: String, pub token: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct TeamsDetailItemDto { pub struct TeamsDetailItemDto {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub leader: TeamMemberDto, pub leader: TeamMemberDto,
pub is_open: bool, pub is_open: bool,
pub max_members: Option<i32>, pub max_members: Option<i32>,
pub current_member_count: i32, pub current_member_count: i32,
pub skills_required: Option<Vec<String>>, pub skills_required: Option<Vec<String>>,
pub location: Option<String>, pub location: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub website_url: Option<String>, pub website_url: Option<String>,
pub github_url: Option<String>, pub github_url: Option<String>,
pub members: Option<Vec<TeamMemberDto>>, pub members: Option<Vec<TeamMemberDto>>,
pub is_active: bool, pub is_active: bool,
pub created_at: String, pub created_at: String,
pub updated_at: String, pub updated_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct MemberTeamsDetailItemDto { pub struct MemberTeamsDetailItemDto {
pub id: String, pub id: String,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub leader: TeamMemberDto, pub leader: TeamMemberDto,
pub is_open: bool, pub is_open: bool,
pub max_members: Option<i32>, pub max_members: Option<i32>,
pub current_member_count: i32, pub current_member_count: i32,
pub skills_required: Option<Vec<String>>, pub skills_required: Option<Vec<String>>,
pub location: Option<String>, pub location: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub website_url: Option<String>, pub website_url: Option<String>,
pub github_url: Option<String>, pub github_url: Option<String>,
pub members: Vec<TeamMemberDto>, // Always include members for authenticated users pub members: Vec<TeamMemberDto>, // Always include members for authenticated users
pub is_active: bool, pub is_active: bool,
pub created_at: String, pub created_at: String,
pub updated_at: String, pub updated_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
@@ -179,168 +179,106 @@ pub struct PublicTeamsDetailItemDto {
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct TeamMemberDto { pub struct TeamMemberDto {
pub id: String, pub id: String,
pub user_id: String, pub user_id: String,
pub fullname: String, pub fullname: String,
pub email: Option<String>, pub email: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub role: String, pub role: String,
pub skills: Option<Vec<String>>, pub skills: Option<Vec<String>>,
pub joined_at: String, pub joined_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct TeamInvitationDto { pub struct TeamInvitationDto {
pub id: String, pub id: String,
pub team_id: String, pub team_id: String,
pub team_name: String, pub team_name: String,
pub email: String, pub email: String,
pub inviter_name: String, pub inviter_name: String,
pub status: String, pub status: String,
pub expires_at: String, pub expires_at: String,
pub invited_at: String, pub invited_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TeamsDetailQueryDto { pub struct TeamsDetailQueryDto {
pub id: Thing, pub id: Thing,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub leader_id: Thing, pub leader_id: Thing,
pub is_open: bool, pub is_open: bool,
pub max_members: Option<i32>, pub max_members: Option<i32>,
pub skills_required: Option<Vec<String>>, pub skills_required: Option<Vec<String>>,
pub location: Option<String>, pub location: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub website_url: Option<String>, pub website_url: Option<String>,
pub github_url: Option<String>, pub github_url: Option<String>,
pub is_active: bool, pub is_active: bool,
pub is_deleted: bool, pub is_deleted: bool,
pub created_at: String, pub created_at: String,
pub updated_at: String, pub updated_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TeamsListQueryDto { pub struct TeamsListQueryDto {
pub id: Thing, pub id: Thing,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub leader_id: Thing, pub leader_id: Thing,
pub is_open: bool, pub is_open: bool,
pub max_members: Option<i32>, pub max_members: Option<i32>,
pub skills_required: Option<Vec<String>>, pub skills_required: Option<Vec<String>>,
pub location: Option<String>, pub location: Option<String>,
pub avatar: Option<String>, pub avatar: Option<String>,
pub created_at: String, pub created_at: String,
pub updated_at: String, pub updated_at: String,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TeamMembersQueryDto { pub struct TeamMembersQueryDto {
pub id: Thing, pub id: Thing,
pub team_id: Thing, pub team_id: Thing,
pub user_id: Thing, pub user_id: Thing,
pub role: String, pub role: String,
pub joined_at: String, pub joined_at: String,
pub is_active: bool, pub is_active: bool,
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TeamInvitationsQueryDto { pub struct TeamInvitationsQueryDto {
pub id: Thing, pub id: Thing,
pub team_id: Thing, pub team_id: Thing,
pub email: String, pub email: String,
pub inviter_id: Thing, pub inviter_id: Thing,
pub invite_code: String, pub invite_code: String,
pub expires_at: String, pub expires_at: String,
pub status: String, pub status: String,
pub invited_at: String, pub invited_at: String,
pub accepted_at: Option<String>, pub accepted_at: Option<String>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct TeamsSearchQueryDto { pub struct TeamsSearchQueryDto {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub query: Option<String>, pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub open: Option<bool>, pub open: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub skills: Option<Vec<String>>, pub skills: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>, pub location: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub page: Option<i64>, pub page: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub per_page: Option<i64>, pub per_page: Option<i64>,
} }
impl TeamsDetailQueryDto {
pub fn from(self) -> Self {
self
}
}
impl TeamsListQueryDto {
pub fn from(self) -> TeamsListItemDto {
TeamsListItemDto {
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: 1,
max_members: self.max_members,
skills_required: self.skills_required,
location: self.location,
avatar: self.avatar,
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)] #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
pub struct AdminTeamsListItemDto { pub struct AdminTeamsListItemDto {
pub id: String, pub id: String,
@@ -381,9 +319,39 @@ pub struct AdminTeamsDetailItemDto {
pub updated_at: String, pub updated_at: String,
} }
impl TeamsDetailQueryDto {
pub fn into_detail_dto(self) -> Self {
self
}
}
impl TeamsListQueryDto { impl TeamsListQueryDto {
pub fn to_admin_list_dto(self) -> AdminTeamsListItemDto { pub fn into_list_item_dto(self) -> TeamsListItemDto {
let members = vec![]; TeamsListItemDto {
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: 1,
max_members: self.max_members,
skills_required: self.skills_required,
location: self.location,
avatar: self.avatar,
created_at: self.created_at,
}
}
pub fn into_admin_list_dto(self) -> AdminTeamsListItemDto {
AdminTeamsListItemDto { AdminTeamsListItemDto {
id: self.id.id.to_raw(), id: self.id.id.to_raw(),
name: self.name, name: self.name,
@@ -399,7 +367,7 @@ impl TeamsListQueryDto {
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: members.len() as i32 + 1, current_member_count: 1, // Placeholder; adjust based on actual member count
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,
@@ -414,7 +382,7 @@ impl TeamsListQueryDto {
} }
impl TeamsDetailQueryDto { impl TeamsDetailQueryDto {
pub fn to_admin_detail_dto(self, members: Vec<TeamMemberDto>) -> AdminTeamsDetailItemDto { pub fn into_admin_detail_dto(self, members: Vec<TeamMemberDto>) -> AdminTeamsDetailItemDto {
AdminTeamsDetailItemDto { AdminTeamsDetailItemDto {
id: self.id.id.to_raw(), id: self.id.id.to_raw(),
name: self.name, name: self.name,
@@ -445,4 +413,3 @@ impl TeamsDetailQueryDto {
} }
} }
} }
}
+270 -265
View File
@@ -110,167 +110,167 @@ impl TeamsService {
impl TeamsServiceTrait for TeamsService { impl TeamsServiceTrait for TeamsService {
fn get_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> { fn get_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
Box::pin(async move { Box::pin(async move {
let repo = TeamsRepository::new(&state); let repo = TeamsRepository::new(&state);
match repo.query_team_list(meta).await { match repo.query_team_list(meta).await {
Ok(data) => { Ok(data) => {
let response = ResponseListSuccessDto { let response = ResponseListSuccessDto {
data: data.data, data: data.data,
meta: data.meta, meta: data.meta,
}; };
success_list_response(response) 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>> { fn get_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
Box::pin(async move { Box::pin(async move {
if Uuid::parse_str(&id).is_err() { if Uuid::parse_str(&id).is_err() {
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format"); 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 })
} }
let repo = TeamsRepository::new(&state); Ok(_) => common_response(StatusCode::NOT_FOUND, "Team not found"),
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id); Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
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>> { fn get_member_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
Box::pin(async move { Box::pin(async move {
let repo = TeamsRepository::new(&state); let repo = TeamsRepository::new(&state);
match repo.query_team_list(meta).await { match repo.query_team_list(meta).await {
Ok(data) => { Ok(data) => {
let response = ResponseListSuccessDto { let response = ResponseListSuccessDto {
data: data.data, data: data.data,
meta: data.meta, meta: data.meta,
}; };
success_list_response(response) 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_member_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> { fn get_member_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
Box::pin(async move { Box::pin(async move {
if Uuid::parse_str(&id).is_err() { if Uuid::parse_str(&id).is_err() {
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format"); return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
} }
let repo = TeamsRepository::new(&state); let repo = TeamsRepository::new(&state);
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id); let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id);
match repo.query_team_by_id(&thing_id).await { match repo.query_team_by_id(&thing_id).await {
Ok(team) if !team.is_deleted => { Ok(team) if !team.is_deleted => {
let members = repo.query_team_members(&team.id).await.unwrap_or_default(); let members = repo.query_team_members(&team.id).await.unwrap_or_default();
// For member team details, include all information including members list // For member team details, include all information including members list
let mut member_dtos = Vec::new(); let mut member_dtos = Vec::new();
for member in members { for member in members {
match Self::get_user_info_with_privacy( match Self::get_user_info_with_privacy(
&member.user_id.id.to_raw(), &member.user_id.id.to_raw(),
"system", // In member context, we show all user info "system", // In member context, we show all user info
true, // 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, &state,
).await { ).await {
Ok(mut leader_dto) => { Ok(mut member_dto) => {
leader_dto.role = "leader".to_string(); member_dto.role = member.role;
leader_dto member_dto.joined_at = member.joined_at;
member_dtos.push(member_dto);
} }
Err(_) => TeamMemberDto { Err(_) => continue,
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()), // 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>> { fn get_public_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
@@ -372,8 +372,8 @@ impl TeamsServiceTrait for TeamsService {
match repo.query_create_invitation(invitation).await { match repo.query_create_invitation(invitation).await {
Ok(_) => { Ok(_) => {
let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await { let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await {
Ok(user) => user, Ok(user) => user,
Err(_) => continue, Err(_) => continue,
}; };
if let Err(e) = Self::send_invitation_email( if let Err(e) = Self::send_invitation_email(
&team_schema.name, &team_schema.name,
@@ -538,8 +538,8 @@ impl TeamsServiceTrait for TeamsService {
match repo.query_create_invitation(invitation).await { match repo.query_create_invitation(invitation).await {
Ok(_) => { Ok(_) => {
let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await { let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await {
Ok(user) => user, Ok(user) => user,
Err(_) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get inviter user information"), Err(_) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get inviter user information"),
}; };
if let Err(e) = Self::send_invitation_email( if let Err(e) = Self::send_invitation_email(
&team.name, &team.name,
@@ -759,6 +759,11 @@ impl TeamsServiceTrait for TeamsService {
}; };
success_list_response(response) success_list_response(response)
} }
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
})
}
fn get_admin_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> { fn get_admin_team_list(state: &AppState, meta: MetaRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
Box::pin(async move { Box::pin(async move {
@@ -777,129 +782,129 @@ impl TeamsServiceTrait for TeamsService {
} }
fn get_admin_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> { fn get_admin_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
let state = state.to_owned(); let state = state.to_owned();
Box::pin(async move { Box::pin(async move {
if Uuid::parse_str(&id).is_err() { if Uuid::parse_str(&id).is_err() {
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format"); return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
} }
let repo = TeamsRepository::new(&state); let repo = TeamsRepository::new(&state);
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id); let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id);
match repo.query_team_by_id(&thing_id).await { match repo.query_team_by_id(&thing_id).await {
Ok(team) if !team.is_deleted => { Ok(team) if !team.is_deleted => {
let members = repo.query_team_members(&team.id).await.unwrap_or_default(); let members = repo.query_team_members(&team.id).await.unwrap_or_default();
let mut member_dtos = Vec::new(); let mut member_dtos = Vec::new();
for member in members { 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( match Self::get_user_info_with_privacy(
&team.leader_id.id.to_raw(), &member.user_id.id.to_raw(),
"system", "system", // Admin context - show all sensitive data
true, true, // Admin context - always show sensitive data
&state, &state,
).await { ).await {
Ok(mut leader_dto) => { Ok(mut member_dto) => {
leader_dto.role = "leader".to_string(); member_dto.role = member.role;
member_dtos.insert(0, leader_dto); member_dto.joined_at = member.joined_at;
member_dtos.push(member_dto);
} }
Err(_) => {} Err(_) => continue,
} }
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>> { // Add leader with full sensitive info
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( match Self::get_user_info_with_privacy(
&member.user_id.id.to_raw(), &team.leader_id.id.to_raw(),
"system", // Admin context - show all sensitive data "system",
true, // Admin context - always show sensitive data true,
&state, &state,
).await { ).await {
Ok(mut member_dto) => { Ok(mut leader_dto) => {
member_dto.role = member.role; leader_dto.role = "leader".to_string();
member_dto.joined_at = member.joined_at; member_dtos.insert(0, leader_dto);
member_dtos.push(member_dto);
} }
Err(_) => continue, Err(_) => {}
} }
}
success_response(ResponseSuccessDto { data: member_dtos })
})
}
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 team_dto = team.to_admin_detail_dto(member_dtos);
let teams = match repo.query_teams_by_user(&user_thing).await { success_response(ResponseSuccessDto { data: team_dto })
Ok(teams) => teams, }
Err(e) => { Ok(_) => common_response(StatusCode::NOT_FOUND, "Team not found"),
error!("Failed to query user teams: {}", e); Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
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")
},
}
})
} }
})
}
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 })
})
}
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")
},
}
})
}
} }