diff --git a/imphnen-iam/src/v1/teams/teams_controller.rs b/imphnen-iam/src/v1/teams/teams_controller.rs index 6d27f8c..08b4b1a 100644 --- a/imphnen-iam/src/v1/teams/teams_controller.rs +++ b/imphnen-iam/src/v1/teams/teams_controller.rs @@ -3,7 +3,7 @@ use crate::{ MessageResponseDto, ResponseListSuccessDto, ResponseSuccessDto, TeamsCreateRequestDto, TeamsDetailItemDto, TeamsListItemDto, permissions_guard, TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto, - TeamMemberDto, TeamsSearchQueryDto + TeamMemberDto, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto }; use axum::extract::Path; use axum::http::HeaderMap; @@ -250,7 +250,7 @@ pub async fn post_accept_invitation( ("per_page" = Option, Query, description = "Items per page"), ), responses( - (status = 200, description = "Search teams", body = ResponseListSuccessDto>) + (status = 200, description = "Search teams", body = ResponseListSuccessDto>) ), tag = "Teams" )] @@ -321,4 +321,46 @@ pub async fn post_leave_team( Ok((claims, state)) => TeamsService::leave_team(&state, claims, id).await, Err(response) => response, } +} + +#[utoipa::path( + get, + path = "/v1/teams/public", + params( + ("page" = Option, Query, description = "Page number"), + ("per_page" = Option, Query, description = "Items per page"), + ("search" = Option, Query, description = "Search keyword"), + ("sort_by" = Option, Query, description = "Sort by field"), + ("order" = Option, Query, description = "Order ASC or DESC"), + ("filter" = Option, Query, description = "Filter value"), + ("filter_by" = Option, Query, description = "Field to filter by"), + ), + responses( + (status = 200, description = "Get public team list", body = ResponseListSuccessDto>) + ), + tag = "Teams" +)] +pub async fn get_public_team_list( + Extension(state): Extension, + axum::extract::Query(meta): axum::extract::Query, +) -> impl IntoResponse { + TeamsService::get_team_list(&state, meta).await +} + +#[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) + ), + tag = "Teams" +)] +pub async fn get_public_team_by_id( + Extension(state): Extension, + Path(id): Path, +) -> impl IntoResponse { + TeamsService::get_team_by_id(&state, id).await } \ No newline at end of file diff --git a/imphnen-iam/src/v1/teams/teams_dto.rs b/imphnen-iam/src/v1/teams/teams_dto.rs index e1657a7..e626bb9 100644 --- a/imphnen-iam/src/v1/teams/teams_dto.rs +++ b/imphnen-iam/src/v1/teams/teams_dto.rs @@ -112,17 +112,49 @@ pub struct TeamsDetailItemDto { #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct TeamsListItemDto { - pub id: String, - pub name: String, - pub description: Option, - pub leader: TeamMemberDto, - pub is_open: bool, - pub current_member_count: i32, - pub max_members: Option, - pub skills_required: Option>, - pub location: Option, - pub avatar: Option, - pub created_at: String, + pub id: String, + pub name: String, + pub description: Option, + pub leader: TeamMemberDto, + pub is_open: bool, + pub current_member_count: i32, + pub max_members: Option, + pub skills_required: Option>, + pub location: Option, + pub avatar: Option, + pub created_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct PublicTeamsListItemDto { + pub id: String, + pub name: String, + pub description: Option, + pub is_open: bool, + pub current_member_count: i32, + pub max_members: Option, + pub skills_required: Option>, + pub location: Option, + pub avatar: Option, + pub created_at: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] +pub struct PublicTeamsDetailItemDto { + pub id: String, + pub name: String, + pub description: Option, + pub is_open: bool, + pub max_members: Option, + pub current_member_count: i32, + pub skills_required: Option>, + pub location: Option, + pub avatar: Option, + pub website_url: Option, + pub github_url: Option, + pub is_active: bool, + pub created_at: String, + pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] diff --git a/imphnen-iam/src/v1/teams/teams_service.rs b/imphnen-iam/src/v1/teams/teams_service.rs index 63be18f..ca22797 100644 --- a/imphnen-iam/src/v1/teams/teams_service.rs +++ b/imphnen-iam/src/v1/teams/teams_service.rs @@ -2,7 +2,7 @@ use super::{ TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto, TeamAcceptInvitationRequestDto, TeamsDetailItemDto, TeamMemberDto, TeamsRepository, TeamsSchema, TeamMembersSchema, - TeamInvitationsSchema, TeamsSearchQueryDto + TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto }; use crate::{ AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto, @@ -23,6 +23,8 @@ use chrono::Utc; pub trait TeamsServiceTrait: Send + Sync + 'static { fn get_team_list(state: &AppState, meta: MetaRequestDto) -> Pin + Send>>; fn get_team_by_id(state: &AppState, id: String) -> Pin + Send>>; + fn get_public_team_list(state: &AppState, meta: MetaRequestDto) -> Pin + Send>>; + fn get_public_team_by_id(state: &AppState, id: String) -> Pin + Send>>; fn create_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, new_team: TeamsCreateRequestDto) -> Pin + Send>>; fn update_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, id: String, team: TeamsUpdateRequestDto) -> Pin + Send>>; fn delete_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, id: String) -> Pin + Send>>; @@ -128,6 +130,8 @@ impl TeamsServiceTrait for TeamsService { 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, @@ -163,6 +167,60 @@ impl TeamsServiceTrait for TeamsService { }) } + fn get_public_team_list(state: &AppState, meta: MetaRequestDto) -> Pin + 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_public_team_by_id(state: &AppState, id: String) -> Pin + 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 = PublicTeamsDetailItemDto { + id: team.id.id.to_raw(), + name: team.name, + description: team.description, + 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, + 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 create_team( state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims,