- Updated HackathonSubmissionsSchema to use Option types for team_id, project_name, description, technologies, submission_status, and submitted_at. - Modified seed_hackathons and seed_test_submission scripts to accommodate new optional fields. - Added routes for participant registration and listing in hackathon_controller. - Implemented register_participant and list_participants functions in hackathon_controller. - Introduced HackathonParticipantSchema and corresponding DTOs for participant management. - Enhanced HackathonRepository with CRUD operations for hackathon participants. - Updated HackathonService to include methods for participant registration and listing. - Refactored TeamsService to allow admin-level updates and invitations, bypassing leader-only restrictions. - Added validation for member emails in TeamsCreateRequestDto and TeamInviteRequestDto.
1061 lines
36 KiB
Rust
1061 lines
36 KiB
Rust
use super::{
|
|
TeamsCreateRequestDto, TeamsUpdateRequestDto, TeamInviteRequestDto,
|
|
TeamAcceptInvitationRequestDto, TeamsDetailItemDto, MemberTeamsDetailItemDto,
|
|
TeamMemberDto, TeamsRepository, TeamsSchema, TeamMembersSchema,
|
|
TeamInvitationsSchema, TeamsSearchQueryDto, PublicTeamsDetailItemDto, AdminTeamsListItemDto
|
|
};
|
|
use crate::{
|
|
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
|
|
common_response, success_list_response, success_response, validate_request,
|
|
UsersRepository
|
|
};
|
|
use axum::{http::StatusCode, response::Response};
|
|
use imphnen_libs::{ResourceEnum, send_email};
|
|
use imphnen_utils::{make_thing_from_enum, OtpManager};
|
|
use uuid::Uuid;
|
|
use std::pin::Pin;
|
|
use std::future::Future;
|
|
use anyhow::Result;
|
|
use tracing::{info, error};
|
|
use serde_json::json;
|
|
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>>;
|
|
fn update_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, id: String, team: TeamsUpdateRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
fn update_team_admin(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, id: String, team: TeamsUpdateRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
fn delete_team(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
fn delete_team_admin(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
fn invite_team_members(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String, invite: TeamInviteRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
fn invite_team_members_admin(state: &AppState, claims: imphnen_libs::jsonwebtoken::Claims, team_id: String, invite: TeamInviteRequestDto) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
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>>;
|
|
fn get_admin_team_members(state: &AppState, team_id: String) -> Pin<Box<dyn Future<Output = Response> + Send>>;
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct TeamsService;
|
|
|
|
impl TeamsService {
|
|
async fn send_invitation_email(
|
|
team_name: &str,
|
|
inviter_name: &str,
|
|
email: &str,
|
|
token: &str,
|
|
is_existing_user: bool,
|
|
) -> Result<()> {
|
|
let subject = format!("Invitation to join team: {}", team_name);
|
|
|
|
let (action_text, action_url) = if is_existing_user {
|
|
("Login and Accept Invitation", format!("https://app.example.com/login?redirect=/teams/invite/{}", token))
|
|
} else {
|
|
("Register and Join Team", format!("https://app.example.com/register?team_token={}", token))
|
|
};
|
|
|
|
let body = format!(
|
|
"Hello,\n\n\
|
|
You have been invited by {} to join the team '{}'.\n\n\
|
|
{}\n\
|
|
{}\n\n\
|
|
This invitation will expire in 72 hours.\n\n\
|
|
Best regards,\n\
|
|
The Team",
|
|
inviter_name, team_name, action_text, action_url
|
|
);
|
|
|
|
send_email(email, &subject, &body)
|
|
.map_err(|e| anyhow::anyhow!("Failed to send invitation email: {}", e))?;
|
|
|
|
info!("Invitation email sent to: {}", email);
|
|
Ok(())
|
|
}
|
|
|
|
async fn generate_invitation_token() -> String {
|
|
format!("team_{}_{}", Uuid::new_v4(), OtpManager::generate_otp().code)
|
|
}
|
|
|
|
async fn get_user_info_with_privacy(
|
|
user_id: &str,
|
|
requester_user_id: &str,
|
|
is_team_member: bool,
|
|
state: &AppState,
|
|
) -> Result<TeamMemberDto> {
|
|
let users_repo = UsersRepository::new(state);
|
|
let user_thing = make_thing_from_enum(ResourceEnum::Users, user_id);
|
|
let user = users_repo.query_user_by_id(&user_thing).await?;
|
|
|
|
let show_sensitive_data = is_team_member || user_id == requester_user_id;
|
|
|
|
Ok(TeamMemberDto {
|
|
id: String::new(),
|
|
user_id: user.id.id.to_raw(),
|
|
fullname: user.fullname,
|
|
email: if show_sensitive_data { Some(user.email) } else { None },
|
|
avatar: user.avatar,
|
|
role: "member".to_string(),
|
|
skills: user.skills,
|
|
joined_at: user.created_at,
|
|
})
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
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 id.trim().is_empty() {
|
|
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 team_thing = make_thing_from_enum(ResourceEnum::Teams, &team.id.id.to_raw());
|
|
let members = repo.query_team_members(&team_thing).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 {
|
|
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 id.trim().is_empty() {
|
|
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 team_thing = make_thing_from_enum(ResourceEnum::Teams, &team.id.id.to_raw());
|
|
let members = repo.query_team_members(&team_thing).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();
|
|
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(),
|
|
}
|
|
};
|
|
|
|
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_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: 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();
|
|
Box::pin(async move {
|
|
let repo = TeamsRepository::new(&state);
|
|
match repo.query_team_list(meta).await {
|
|
Ok(mut data) => {
|
|
// Calculate actual member count for each team
|
|
for team in &mut data.data {
|
|
let team_thing = make_thing_from_enum(ResourceEnum::Teams, &team.id);
|
|
let members = repo.query_team_members(&team_thing).await.unwrap_or_default();
|
|
team.current_member_count = members.len() as i32 + 1; // +1 for leader
|
|
}
|
|
|
|
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<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if id.trim().is_empty() {
|
|
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 team_thing = make_thing_from_enum(ResourceEnum::Teams, &team.id.id.to_raw());
|
|
let members = repo.query_team_members(&team_thing).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 {
|
|
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,
|
|
new_team: TeamsCreateRequestDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if let Err((status, message)) = validate_request(&new_team) {
|
|
return common_response(status, &message);
|
|
}
|
|
|
|
let repo = TeamsRepository::new(&state);
|
|
let users_repo = UsersRepository::new(&state);
|
|
|
|
let team_schema = TeamsSchema::create(new_team.clone(), claims.user_id.clone());
|
|
|
|
match repo.query_create_team(team_schema.clone()).await {
|
|
Ok(_) => {
|
|
let leader_member = TeamMembersSchema::create(
|
|
team_schema.id.id.to_raw(),
|
|
claims.user_id.clone(),
|
|
Some("leader".to_string()),
|
|
);
|
|
|
|
if let Err(e) = repo.query_add_team_member(leader_member).await {
|
|
error!("Failed to add team leader as member: {}", e);
|
|
}
|
|
|
|
let mut successful_invites = Vec::new();
|
|
let mut failed_invites = Vec::new();
|
|
|
|
for email in new_team.member_emails {
|
|
let existing_user = users_repo.query_user_by_email(email.clone()).await.ok();
|
|
let is_existing_user = existing_user.is_some();
|
|
|
|
let token = Self::generate_invitation_token().await;
|
|
let invitation = TeamInvitationsSchema::create(
|
|
team_schema.id.id.to_raw(),
|
|
email.clone(),
|
|
claims.user_id.clone(),
|
|
token.clone(),
|
|
);
|
|
|
|
match repo.query_create_invitation(invitation).await {
|
|
Ok(_) => {
|
|
let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await {
|
|
Ok(user) => user,
|
|
Err(_) => continue,
|
|
};
|
|
if let Err(e) = Self::send_invitation_email(
|
|
&team_schema.name,
|
|
&inviter_user.fullname,
|
|
&email,
|
|
&token,
|
|
is_existing_user,
|
|
).await {
|
|
error!("Failed to send invitation email to {}: {}", email, e);
|
|
failed_invites.push(email);
|
|
} else {
|
|
successful_invites.push(email);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to create invitation for {}: {}", email, e);
|
|
failed_invites.push(email);
|
|
}
|
|
}
|
|
}
|
|
|
|
let response_data = json!({
|
|
"team_id": team_schema.id.id.to_raw(),
|
|
"message": "Team created successfully",
|
|
"invitations_sent": successful_invites.len(),
|
|
"invitations_failed": failed_invites.len(),
|
|
"failed_emails": failed_invites
|
|
});
|
|
|
|
imphnen_utils::success_created_response(ResponseSuccessDto { data: response_data })
|
|
}
|
|
Err(err) => {
|
|
error!("Failed to create team: {}", err);
|
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, &err.to_string())
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn update_team(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
id: String,
|
|
team: TeamsUpdateRequestDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if id.trim().is_empty() {
|
|
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
|
|
}
|
|
|
|
if let Err((status, message)) = validate_request(&team) {
|
|
return common_response(status, &message);
|
|
}
|
|
|
|
let repo = TeamsRepository::new(&state);
|
|
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id);
|
|
|
|
let current_team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
// Allow update if requester is leader
|
|
if current_team.leader_id.id.to_raw() != claims.user_id {
|
|
// Not leader; deny here (admin endpoints should use update_team_admin)
|
|
return common_response(StatusCode::FORBIDDEN, "Only team leader can update team");
|
|
}
|
|
|
|
let updated_team = TeamsSchema {
|
|
id: current_team.id,
|
|
leader_id: current_team.leader_id,
|
|
is_active: current_team.is_active,
|
|
is_deleted: current_team.is_deleted,
|
|
created_at: current_team.created_at,
|
|
..TeamsSchema::default()
|
|
}.update(team);
|
|
|
|
match repo.query_update_team(updated_team).await {
|
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn update_team_admin(
|
|
state: &AppState,
|
|
_claims: imphnen_libs::jsonwebtoken::Claims,
|
|
id: String,
|
|
team: TeamsUpdateRequestDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if id.trim().is_empty() {
|
|
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
|
|
}
|
|
|
|
if let Err((status, message)) = validate_request(&team) {
|
|
return common_response(status, &message);
|
|
}
|
|
|
|
let repo = TeamsRepository::new(&state);
|
|
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &id);
|
|
let current_team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
let updated_team = TeamsSchema {
|
|
id: current_team.id,
|
|
leader_id: current_team.leader_id,
|
|
is_active: current_team.is_active,
|
|
is_deleted: current_team.is_deleted,
|
|
created_at: current_team.created_at,
|
|
..TeamsSchema::default()
|
|
}.update(team);
|
|
|
|
match repo.query_update_team(updated_team).await {
|
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn delete_team(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
id: String,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if id.trim().is_empty() {
|
|
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);
|
|
|
|
let team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
if team.leader_id.id.to_raw() != claims.user_id {
|
|
return common_response(StatusCode::FORBIDDEN, "Only team leader can delete team");
|
|
}
|
|
|
|
match repo.query_delete_team(id).await {
|
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn delete_team_admin(
|
|
state: &AppState,
|
|
_claims: imphnen_libs::jsonwebtoken::Claims,
|
|
id: String,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if id.trim().is_empty() {
|
|
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);
|
|
let _team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
match repo.query_delete_team(id).await {
|
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn invite_team_members(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
team_id: String,
|
|
invite: TeamInviteRequestDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if team_id.trim().is_empty() {
|
|
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
|
|
}
|
|
|
|
if let Err((status, message)) = validate_request(&invite) {
|
|
return common_response(status, &message);
|
|
}
|
|
|
|
let repo = TeamsRepository::new(&state);
|
|
let users_repo = UsersRepository::new(&state);
|
|
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &team_id);
|
|
|
|
let team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
let user_thing = make_thing_from_enum(ResourceEnum::Users, &claims.user_id);
|
|
let is_member = repo.query_is_team_member(&thing_id, &user_thing).await.unwrap_or(false);
|
|
let is_leader = team.leader_id.id.to_raw() == claims.user_id;
|
|
|
|
if !is_member && !is_leader {
|
|
return common_response(StatusCode::FORBIDDEN, "Only team members can invite others");
|
|
}
|
|
|
|
let mut successful_invites = Vec::new();
|
|
let mut failed_invites = Vec::new();
|
|
|
|
for email in invite.member_emails {
|
|
let existing_user = users_repo.query_user_by_email(email.clone()).await.ok();
|
|
let is_existing_user = existing_user.is_some();
|
|
|
|
let token = Self::generate_invitation_token().await;
|
|
let invitation = TeamInvitationsSchema::create(
|
|
team_id.clone(),
|
|
email.clone(),
|
|
claims.user_id.clone(),
|
|
token.clone(),
|
|
);
|
|
|
|
match repo.query_create_invitation(invitation).await {
|
|
Ok(_) => {
|
|
let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await {
|
|
Ok(user) => user,
|
|
Err(_) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get inviter user information"),
|
|
};
|
|
if let Err(e) = Self::send_invitation_email(
|
|
&team.name,
|
|
&inviter_user.fullname,
|
|
&email,
|
|
&token,
|
|
is_existing_user,
|
|
).await {
|
|
error!("Failed to send invitation email to {}: {}", email, e);
|
|
failed_invites.push(email);
|
|
} else {
|
|
successful_invites.push(email);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to create invitation for {}: {}", email, e);
|
|
failed_invites.push(email);
|
|
}
|
|
}
|
|
}
|
|
|
|
let response_data = json!({
|
|
"invitations_sent": successful_invites.len(),
|
|
"invitations_failed": failed_invites.len(),
|
|
"failed_emails": failed_invites
|
|
});
|
|
|
|
success_response(ResponseSuccessDto { data: response_data })
|
|
})
|
|
}
|
|
|
|
fn invite_team_members_admin(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
team_id: String,
|
|
invite: TeamInviteRequestDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if team_id.trim().is_empty() {
|
|
return common_response(StatusCode::BAD_REQUEST, "Invalid Team ID format");
|
|
}
|
|
|
|
if let Err((status, message)) = validate_request(&invite) {
|
|
return common_response(status, &message);
|
|
}
|
|
|
|
let repo = TeamsRepository::new(&state);
|
|
let users_repo = UsersRepository::new(&state);
|
|
let thing_id = make_thing_from_enum(ResourceEnum::Teams, &team_id);
|
|
let team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
let mut successful_invites = Vec::new();
|
|
let mut failed_invites = Vec::new();
|
|
|
|
for email in invite.member_emails {
|
|
let existing_user = users_repo.query_user_by_email(email.clone()).await.ok();
|
|
let is_existing_user = existing_user.is_some();
|
|
let token = Self::generate_invitation_token().await;
|
|
let invitation = TeamInvitationsSchema::create(
|
|
team_id.clone(),
|
|
email.clone(),
|
|
claims.user_id.clone(),
|
|
token.clone(),
|
|
);
|
|
|
|
match repo.query_create_invitation(invitation).await {
|
|
Ok(_) => {
|
|
let inviter_user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await {
|
|
Ok(user) => user,
|
|
Err(_) => return common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to get inviter user information"),
|
|
};
|
|
if let Err(e) = Self::send_invitation_email(
|
|
&team.name,
|
|
&inviter_user.fullname,
|
|
&email,
|
|
&token,
|
|
is_existing_user,
|
|
).await {
|
|
error!("Failed to send invitation email to {}: {}", email, e);
|
|
failed_invites.push(email);
|
|
} else {
|
|
successful_invites.push(email);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to create invitation for {}: {}", email, e);
|
|
failed_invites.push(email);
|
|
}
|
|
}
|
|
}
|
|
|
|
let response_data = json!({
|
|
"invitations_sent": successful_invites.len(),
|
|
"invitations_failed": failed_invites.len(),
|
|
"failed_emails": failed_invites
|
|
});
|
|
|
|
success_response(ResponseSuccessDto { data: response_data })
|
|
})
|
|
}
|
|
|
|
fn accept_invitation(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
accept: TeamAcceptInvitationRequestDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
let repo = TeamsRepository::new(&state);
|
|
let users_repo = UsersRepository::new(&state);
|
|
|
|
let invitation = match repo.query_invitation_by_token(&accept.token).await {
|
|
Ok(inv) => inv,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Invalid or expired invitation"),
|
|
};
|
|
|
|
if invitation.status != "pending" {
|
|
return common_response(StatusCode::BAD_REQUEST, "Invitation already processed");
|
|
}
|
|
|
|
if Utc::now().timestamp() > invitation.expires_at.parse::<i64>().unwrap_or(0) {
|
|
return common_response(StatusCode::BAD_REQUEST, "Invitation has expired");
|
|
}
|
|
|
|
let user = match users_repo.query_user_by_id(&make_thing_from_enum(ResourceEnum::Users, &claims.user_id)).await {
|
|
Ok(user) => user,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "User not found"),
|
|
};
|
|
|
|
if user.email != invitation.email {
|
|
return common_response(StatusCode::FORBIDDEN, "Invitation email does not match user email");
|
|
}
|
|
|
|
let user_thing = make_thing_from_enum(ResourceEnum::Users, &claims.user_id);
|
|
let team_thing = make_thing_from_enum(ResourceEnum::Teams, &invitation.team_id.id.to_raw());
|
|
let is_already_member = repo.query_is_team_member(&team_thing, &user_thing).await.unwrap_or(false);
|
|
|
|
if is_already_member {
|
|
return common_response(StatusCode::BAD_REQUEST, "User is already a team member");
|
|
}
|
|
|
|
let member_schema = TeamMembersSchema::create(
|
|
invitation.team_id.id.to_raw(),
|
|
claims.user_id,
|
|
None,
|
|
);
|
|
|
|
match repo.query_add_team_member(member_schema).await {
|
|
Ok(_) => {
|
|
let updated_invitation = TeamInvitationsSchema {
|
|
id: invitation.id,
|
|
team_id: invitation.team_id,
|
|
email: invitation.email,
|
|
inviter_id: invitation.inviter_id,
|
|
invite_code: invitation.invite_code,
|
|
expires_at: chrono::DateTime::parse_from_rfc3339(&invitation.expires_at)
|
|
.unwrap_or_default()
|
|
.with_timezone(&Utc),
|
|
status: "accepted".to_string(),
|
|
invited_at: invitation.invited_at,
|
|
accepted_at: Some(chrono::Utc::now().to_rfc3339()),
|
|
};
|
|
|
|
if let Err(e) = repo.query_update_invitation(updated_invitation).await {
|
|
error!("Failed to update invitation status: {}", e);
|
|
}
|
|
|
|
common_response(StatusCode::OK, "Successfully joined the team")
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to add team member: {}", e);
|
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to join team")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn get_team_members(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
team_id: String,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if team_id.trim().is_empty() {
|
|
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 user_thing = make_thing_from_enum(ResourceEnum::Users, &claims.user_id);
|
|
|
|
let team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
let is_member = repo.query_is_team_member(&thing_id, &user_thing).await.unwrap_or(false);
|
|
|
|
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(),
|
|
&claims.user_id,
|
|
is_member,
|
|
&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,
|
|
}
|
|
}
|
|
|
|
if let Ok(mut leader_dto) = Self::get_user_info_with_privacy(
|
|
&team.leader_id.id.to_raw(),
|
|
&claims.user_id,
|
|
is_member,
|
|
&state,
|
|
).await {
|
|
leader_dto.role = "leader".to_string();
|
|
member_dtos.insert(0, leader_dto);
|
|
}
|
|
|
|
success_response(ResponseSuccessDto { data: member_dtos })
|
|
})
|
|
}
|
|
|
|
fn leave_team(
|
|
state: &AppState,
|
|
claims: imphnen_libs::jsonwebtoken::Claims,
|
|
team_id: String,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
if team_id.trim().is_empty() {
|
|
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 user_thing = make_thing_from_enum(ResourceEnum::Users, &claims.user_id);
|
|
|
|
let team = match repo.query_team_by_id(&thing_id).await {
|
|
Ok(team) => team,
|
|
Err(_) => return common_response(StatusCode::NOT_FOUND, "Team not found"),
|
|
};
|
|
|
|
if team.leader_id.id.to_raw() == claims.user_id {
|
|
return common_response(StatusCode::BAD_REQUEST, "Team leader cannot leave the team");
|
|
}
|
|
|
|
let is_member = repo.query_is_team_member(&thing_id, &user_thing).await.unwrap_or(false);
|
|
if !is_member {
|
|
return common_response(StatusCode::BAD_REQUEST, "User is not a team member");
|
|
}
|
|
|
|
match repo.query_remove_team_member(&thing_id, &user_thing).await {
|
|
Ok(msg) => common_response(StatusCode::OK, &msg),
|
|
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn search_teams(
|
|
state: &AppState,
|
|
search_params: TeamsSearchQueryDto,
|
|
) -> Pin<Box<dyn Future<Output = Response> + Send>> {
|
|
let state = state.to_owned();
|
|
Box::pin(async move {
|
|
let repo = TeamsRepository::new(&state);
|
|
match repo.query_search_teams(search_params).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_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.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()),
|
|
}
|
|
})
|
|
}
|
|
|
|
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 id.trim().is_empty() {
|
|
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 team_thing = make_thing_from_enum(ResourceEnum::Teams, &team.id.id.to_raw());
|
|
let members = repo.query_team_members(&team_thing).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
|
|
if let Ok(mut leader_dto) = Self::get_user_info_with_privacy(
|
|
&team.leader_id.id.to_raw(),
|
|
"system",
|
|
true,
|
|
&state,
|
|
).await {
|
|
leader_dto.role = "leader".to_string();
|
|
member_dtos.insert(0, leader_dto);
|
|
}
|
|
|
|
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"),
|
|
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 team_id.trim().is_empty() {
|
|
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(_) => common_response(StatusCode::OK, &format!("Successfully left team: {}", team.name)),
|
|
Err(e) => {
|
|
error!("Failed to remove team member: {}", e);
|
|
common_response(StatusCode::INTERNAL_SERVER_ERROR, "Failed to leave team")
|
|
},
|
|
}
|
|
})
|
|
}
|
|
} |