feat: Enhance team management and permissions structure with detailed DTOs and service methods
This commit is contained in:
@@ -10,21 +10,8 @@ pub use teams_repository::*;
|
||||
pub use teams_schema::*;
|
||||
pub use teams_service::*;
|
||||
|
||||
use axum::{
|
||||
routing::{delete, get, post, put},
|
||||
Router,
|
||||
};
|
||||
use axum::Router;
|
||||
|
||||
pub fn teams_router() -> Router {
|
||||
Router::new()
|
||||
.route("/", get(get_team_list))
|
||||
.route("/create", post(post_create_team))
|
||||
.route("/detail/{id}", get(get_team_by_id))
|
||||
.route("/update/{id}", put(put_update_team))
|
||||
.route("/delete/{id}", delete(delete_team))
|
||||
.route("/{id}/invite", post(post_invite_team_members))
|
||||
.route("/accept/{token}", post(post_accept_invitation))
|
||||
.route("/search", get(get_public_team_search))
|
||||
.route("/{id}/members", get(get_team_members))
|
||||
.route("/{id}/leave", post(post_leave_team))
|
||||
}
|
||||
@@ -6,9 +6,7 @@ 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;
|
||||
@@ -20,10 +18,10 @@ async fn authenticated<F, Fut>(
|
||||
headers: HeaderMap,
|
||||
state: Extension<AppState>,
|
||||
f: F,
|
||||
) -> impl IntoResponse
|
||||
) -> Response
|
||||
where
|
||||
F: FnOnce(crate::Claims, AppState) -> Fut,
|
||||
Fut: std::future::Future<Output = impl IntoResponse> + Send,
|
||||
Fut: std::future::Future<Output = Response> + Send,
|
||||
{
|
||||
match permissions_guard(headers, state, vec![]).await {
|
||||
Ok((claims, state)) => f(claims, state).await,
|
||||
@@ -35,12 +33,12 @@ where
|
||||
async fn with_perms<F, Fut>(
|
||||
headers: HeaderMap,
|
||||
state: Extension<AppState>,
|
||||
perms: Vec<String>,
|
||||
perms: Vec<PermissionsEnum>,
|
||||
f: F,
|
||||
) -> impl IntoResponse
|
||||
) -> Response
|
||||
where
|
||||
F: FnOnce(crate::Claims, AppState) -> Fut,
|
||||
Fut: std::future::Future<Output = impl IntoResponse> + Send,
|
||||
Fut: std::future::Future<Output = Response> + Send,
|
||||
{
|
||||
match permissions_guard(headers, state, perms).await {
|
||||
Ok((claims, state)) => f(claims, state).await,
|
||||
@@ -73,14 +71,11 @@ pub async fn get_team_list(
|
||||
headers: Option<HeaderMap>,
|
||||
Extension(state): Extension<AppState>,
|
||||
axum::extract::Query(meta): axum::extract::Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Response {
|
||||
let state = state;
|
||||
match headers {
|
||||
Some(headers) => {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state.clone()),
|
||||
vec![],
|
||||
).await {
|
||||
match permissions_guard(headers, axum::Extension(state.clone()), vec![]).await {
|
||||
Ok((_claims, state)) => TeamsService::get_team_list(&state, meta).await,
|
||||
Err(_) => TeamsService::get_public_team_list(&state, meta).await,
|
||||
}
|
||||
@@ -105,14 +100,11 @@ pub async fn get_team_by_id(
|
||||
headers: Option<HeaderMap>,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
) -> Response {
|
||||
let state = state;
|
||||
match headers {
|
||||
Some(headers) => {
|
||||
match permissions_guard(
|
||||
headers,
|
||||
Extension(state.clone()),
|
||||
vec![],
|
||||
).await {
|
||||
match permissions_guard(headers, axum::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,
|
||||
}
|
||||
@@ -343,8 +335,12 @@ pub async fn get_admin_team_list(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
axum::extract::Query(meta): axum::extract::Query<MetaRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
with_perms(headers, Extension(state), vec![PermissionsEnum::ReadListTeams.to_string()], move |_claims, state| TeamsService::get_admin_team_list(&state, meta)).await
|
||||
) -> Response {
|
||||
let state = state;
|
||||
with_perms(headers, axum::Extension(state), vec![PermissionsEnum::ReadListTeams], move |_claims, state| {
|
||||
let response = TeamsService::get_admin_team_list(&state, meta);
|
||||
response
|
||||
}).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -365,8 +361,12 @@ pub async fn get_admin_team_by_id(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
with_perms(headers, Extension(state), vec![PermissionsEnum::ReadDetailTeams.to_string()], move |_claims, state| TeamsService::get_admin_team_by_id(&state, id)).await
|
||||
) -> Response {
|
||||
let state = state;
|
||||
with_perms(headers, axum::Extension(state), vec![PermissionsEnum::ReadDetailTeams], move |_claims, state| {
|
||||
let response = TeamsService::get_admin_team_by_id(&state, id);
|
||||
response
|
||||
}).await
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
@@ -387,6 +387,10 @@ pub async fn get_admin_team_members(
|
||||
headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
with_perms(headers, Extension(state), vec![PermissionsEnum::ReadDetailTeams.to_string()], move |_claims, state| TeamsService::get_admin_team_members(&state, id)).await
|
||||
) -> Response {
|
||||
let state = state;
|
||||
with_perms(headers, axum::Extension(state), vec![PermissionsEnum::ReadDetailTeams], move |_claims, state| {
|
||||
let response = TeamsService::get_admin_team_members(&state, id);
|
||||
response
|
||||
}).await
|
||||
}
|
||||
|
||||
@@ -325,6 +325,32 @@ impl TeamsDetailQueryDto {
|
||||
}
|
||||
}
|
||||
|
||||
impl TeamsListItemDto {
|
||||
pub fn into_list_item_dto(self) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_admin_list_dto(self) -> AdminTeamsListItemDto {
|
||||
AdminTeamsListItemDto {
|
||||
id: self.id,
|
||||
name: self.name,
|
||||
description: self.description,
|
||||
leader: self.leader,
|
||||
is_open: self.is_open,
|
||||
current_member_count: self.current_member_count,
|
||||
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 TeamsListQueryDto {
|
||||
pub fn into_list_item_dto(self) -> TeamsListItemDto {
|
||||
TeamsListItemDto {
|
||||
|
||||
@@ -8,8 +8,7 @@ use imphnen_libs::{
|
||||
};
|
||||
use imphnen_utils::{
|
||||
get_id, DetailQueryBuilder, QueryListBuilder, make_thing_from_enum,
|
||||
build_thing_condition, build_multi_thing_condition, execute_safe_update_query, execute_safe_count_query,
|
||||
ListQueryBuilder
|
||||
build_multi_thing_condition, execute_safe_update_query, execute_safe_count_query,
|
||||
};
|
||||
use surrealdb::sql::Thing;
|
||||
use anyhow::{Result, bail};
|
||||
@@ -52,7 +51,7 @@ impl<'a> TeamsRepository<'a> {
|
||||
let data = result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(TeamsListQueryDto::from)
|
||||
.map(|dto| dto.into_list_item_dto())
|
||||
.collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
@@ -215,7 +214,7 @@ impl<'a> TeamsRepository<'a> {
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
pub async fn query_user_teams(&self, user_id: &Thing) -> Result<Vec<TeamsDetailQueryDto>> {
|
||||
pub async fn query_teams_by_user(&self, user_id: &Thing) -> Result<Vec<TeamsDetailQueryDto>> {
|
||||
let now = Instant::now();
|
||||
let db = &self.state.surrealdb_ws;
|
||||
let sql = format!(
|
||||
@@ -390,7 +389,7 @@ impl<'a> TeamsRepository<'a> {
|
||||
let data = result
|
||||
.data
|
||||
.into_iter()
|
||||
.map(TeamsListQueryDto::from)
|
||||
.map(|dto| dto.into_list_item_dto())
|
||||
.collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data,
|
||||
|
||||
@@ -2,8 +2,7 @@ use super::{
|
||||
TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto,
|
||||
TeamAcceptInvitationRequestDto, TeamsDetailItemDto, MemberTeamsDetailItemDto,
|
||||
TeamMemberDto, TeamsRepository, TeamsSchema, TeamMembersSchema,
|
||||
TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsListItemDto, PublicTeamsDetailItemDto,
|
||||
AdminTeamsListItemDto, AdminTeamsDetailItemDto
|
||||
TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsDetailItemDto, AdminTeamsListItemDto
|
||||
};
|
||||
use crate::{
|
||||
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
||||
@@ -137,6 +136,7 @@ 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();
|
||||
let members_len = members.len();
|
||||
|
||||
// For public team details, only show sensitive info if user is authenticated and part of the team
|
||||
let team_dto = TeamsDetailItemDto {
|
||||
@@ -155,7 +155,7 @@ impl TeamsServiceTrait for TeamsService {
|
||||
},
|
||||
is_open: team.is_open,
|
||||
max_members: team.max_members,
|
||||
current_member_count: members.len() as i32 + 1,
|
||||
current_member_count: members_len as i32 + 1,
|
||||
skills_required: team.skills_required,
|
||||
location: team.location,
|
||||
avatar: team.avatar,
|
||||
@@ -202,6 +202,7 @@ 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();
|
||||
let members_len = members.len();
|
||||
|
||||
// For member team details, include all information including members list
|
||||
let mut member_dtos = Vec::new();
|
||||
@@ -244,16 +245,17 @@ impl TeamsServiceTrait for TeamsService {
|
||||
}
|
||||
};
|
||||
|
||||
let leader_dto_clone = leader_dto.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,
|
||||
leader: leader_dto_clone,
|
||||
is_open: team.is_open,
|
||||
max_members: team.max_members,
|
||||
current_member_count: members.len() as i32 + 1,
|
||||
current_member_count: members_len as i32 + 1,
|
||||
skills_required: team.skills_required,
|
||||
location: team.location,
|
||||
avatar: team.avatar,
|
||||
@@ -300,6 +302,7 @@ 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();
|
||||
let members_len = members.len();
|
||||
|
||||
// For public team details, only show sensitive info if user is authenticated and part of the team
|
||||
let team_dto = PublicTeamsDetailItemDto {
|
||||
@@ -308,7 +311,7 @@ impl TeamsServiceTrait for TeamsService {
|
||||
description: team.description,
|
||||
is_open: team.is_open,
|
||||
max_members: team.max_members,
|
||||
current_member_count: members.len() as i32 + 1,
|
||||
current_member_count: members_len as i32 + 1,
|
||||
skills_required: team.skills_required,
|
||||
location: team.location,
|
||||
avatar: team.avatar,
|
||||
@@ -765,21 +768,21 @@ impl TeamsServiceTrait for TeamsService {
|
||||
}
|
||||
|
||||
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)
|
||||
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.into_list_item_dto().into_admin_list_dto()).collect::<Vec<AdminTeamsListItemDto>>(),
|
||||
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_admin_team_by_id(state: &AppState, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
||||
let state = state.to_owned();
|
||||
@@ -824,7 +827,7 @@ impl TeamsServiceTrait for TeamsService {
|
||||
Err(_) => {}
|
||||
}
|
||||
|
||||
let team_dto = team.to_admin_detail_dto(member_dtos);
|
||||
let team_dto = team.into_admin_detail_dto(member_dtos);
|
||||
success_response(ResponseSuccessDto { data: team_dto })
|
||||
}
|
||||
Ok(_) => common_response(StatusCode::NOT_FOUND, "Team not found"),
|
||||
@@ -899,7 +902,7 @@ impl TeamsServiceTrait for TeamsService {
|
||||
}
|
||||
|
||||
match repo.query_remove_team_member(&team_thing, &user_thing).await {
|
||||
Ok(msg) => common_response(StatusCode::OK, &format!("Successfully left team: {}", team.name)),
|
||||
Ok(_) => 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