Initial commit: VRBattles API
This commit is contained in:
751
src/handlers/teams.rs
Normal file
751
src/handlers/teams.rs
Normal file
@@ -0,0 +1,751 @@
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
Json,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use validator::Validate;
|
||||
|
||||
use crate::{
|
||||
auth::{AuthUser, OptionalAuthUser},
|
||||
error::{AppError, Result},
|
||||
models::team::Team,
|
||||
models::team_player::{PlayerPosition, TeamPlayer},
|
||||
};
|
||||
|
||||
use super::auth::AppState;
|
||||
|
||||
// ============================================================================
|
||||
// Request/Response Types
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct ListTeamsQuery {
|
||||
pub search: Option<String>,
|
||||
pub page: Option<i64>,
|
||||
pub per_page: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TeamListResponse {
|
||||
pub teams: Vec<TeamWithMemberCount>,
|
||||
pub total: i64,
|
||||
pub page: i64,
|
||||
pub per_page: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct TeamWithMemberCount {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub logo: String,
|
||||
pub bio: Option<String>,
|
||||
pub rank: i32,
|
||||
pub mmr: f32,
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
pub member_count: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct CreateTeamRequest {
|
||||
#[validate(length(min = 2, max = 255, message = "Team name must be 2-255 characters"))]
|
||||
pub name: String,
|
||||
pub bio: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Validate)]
|
||||
pub struct UpdateTeamRequest {
|
||||
#[validate(length(min = 2, max = 255, message = "Team name must be 2-255 characters"))]
|
||||
pub name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TeamResponse {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub logo: String,
|
||||
pub bio: Option<String>,
|
||||
pub rank: i32,
|
||||
pub mmr: f32,
|
||||
pub mu: f32,
|
||||
pub sigma: f32,
|
||||
pub ordinal: f32,
|
||||
pub date_created: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<Team> for TeamResponse {
|
||||
fn from(team: Team) -> Self {
|
||||
TeamResponse {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
logo: team.logo,
|
||||
bio: team.bio,
|
||||
rank: team.rank,
|
||||
mmr: team.mmr,
|
||||
mu: team.mu.unwrap_or(25.0),
|
||||
sigma: team.sigma.unwrap_or(8.333),
|
||||
ordinal: team.ordinal.unwrap_or(0.0),
|
||||
date_created: team.date_created,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct TeamDetailResponse {
|
||||
pub team: TeamResponse,
|
||||
pub members: Vec<TeamMemberResponse>,
|
||||
pub is_member: bool,
|
||||
pub is_captain: bool,
|
||||
pub pending_request: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, FromRow)]
|
||||
pub struct TeamMemberResponse {
|
||||
pub id: i32,
|
||||
pub player_id: i32,
|
||||
pub username: String,
|
||||
pub firstname: String,
|
||||
pub lastname: String,
|
||||
pub profile: Option<String>,
|
||||
pub position: String,
|
||||
pub status: i32,
|
||||
pub date_joined: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChangePositionRequest {
|
||||
pub position: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MyTeamResponse {
|
||||
pub team: Option<TeamResponse>,
|
||||
pub position: Option<String>,
|
||||
pub members: Vec<TeamMemberResponse>,
|
||||
pub pending_requests: Vec<TeamMemberResponse>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Handlers
|
||||
// ============================================================================
|
||||
|
||||
/// GET /api/teams
|
||||
/// List all teams with pagination and search
|
||||
pub async fn list_teams(
|
||||
State(state): State<AppState>,
|
||||
Query(query): Query<ListTeamsQuery>,
|
||||
) -> Result<Json<TeamListResponse>> {
|
||||
let page = query.page.unwrap_or(1).max(1);
|
||||
let per_page = query.per_page.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * per_page;
|
||||
|
||||
let search_pattern = query.search.as_ref().map(|s| format!("%{}%", s));
|
||||
|
||||
// Get total count
|
||||
let total: i64 = if let Some(ref pattern) = search_pattern {
|
||||
sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM teams WHERE is_delete = false AND name ILIKE $1"
|
||||
)
|
||||
.bind(pattern)
|
||||
.fetch_one(&state.pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM teams WHERE is_delete = false"
|
||||
)
|
||||
.fetch_one(&state.pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
// Get teams with member count
|
||||
let teams: Vec<TeamWithMemberCount> = if let Some(ref pattern) = search_pattern {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT t.id, t.name, t.logo, t.bio, t.rank, t.mmr, t.date_created,
|
||||
COUNT(tp.id) FILTER (WHERE tp.status = 1) as member_count
|
||||
FROM teams t
|
||||
LEFT JOIN team_players tp ON t.id = tp.team_id
|
||||
WHERE t.is_delete = false AND t.name ILIKE $1
|
||||
GROUP BY t.id
|
||||
ORDER BY t.rank ASC, t.mmr DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#
|
||||
)
|
||||
.bind(pattern)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await?
|
||||
} else {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT t.id, t.name, t.logo, t.bio, t.rank, t.mmr, t.date_created,
|
||||
COUNT(tp.id) FILTER (WHERE tp.status = 1) as member_count
|
||||
FROM teams t
|
||||
LEFT JOIN team_players tp ON t.id = tp.team_id
|
||||
WHERE t.is_delete = false
|
||||
GROUP BY t.id
|
||||
ORDER BY t.rank ASC, t.mmr DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#
|
||||
)
|
||||
.bind(per_page)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await?
|
||||
};
|
||||
|
||||
Ok(Json(TeamListResponse {
|
||||
teams,
|
||||
total,
|
||||
page,
|
||||
per_page,
|
||||
}))
|
||||
}
|
||||
|
||||
/// POST /api/teams
|
||||
/// Create a new team (creator becomes captain)
|
||||
pub async fn create_team(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreateTeamRequest>,
|
||||
) -> Result<(StatusCode, Json<TeamDetailResponse>)> {
|
||||
payload.validate()?;
|
||||
|
||||
// Check if user is already in a team
|
||||
let existing_membership = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM team_players WHERE player_id = $1 AND status = 1"
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if existing_membership.is_some() {
|
||||
return Err(AppError::Conflict("You are already a member of a team".to_string()));
|
||||
}
|
||||
|
||||
// Check if team name already exists
|
||||
let existing_team = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM teams WHERE name = $1 AND is_delete = false"
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if existing_team.is_some() {
|
||||
return Err(AppError::Conflict("Team name already exists".to_string()));
|
||||
}
|
||||
|
||||
// Create team (logo uses default empty string)
|
||||
let team = sqlx::query_as::<_, Team>(
|
||||
r#"
|
||||
INSERT INTO teams (name, bio, logo, rank, mmr)
|
||||
VALUES ($1, $2, '', 0, 1000.0)
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.bio)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Add creator as captain
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO team_players (team_id, player_id, position, status)
|
||||
VALUES ($1, $2, 'captain', 1)
|
||||
"#
|
||||
)
|
||||
.bind(team.id)
|
||||
.bind(user.id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Get members (just the captain)
|
||||
let members = get_team_members(&state.pool, team.id).await?;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(TeamDetailResponse {
|
||||
team: team.into(),
|
||||
members,
|
||||
is_member: true,
|
||||
is_captain: true,
|
||||
pending_request: false,
|
||||
}),
|
||||
))
|
||||
}
|
||||
|
||||
/// GET /api/teams/:id
|
||||
/// Get team details with members
|
||||
pub async fn get_team(
|
||||
OptionalAuthUser(user): OptionalAuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(team_id): Path<i32>,
|
||||
) -> Result<Json<TeamDetailResponse>> {
|
||||
let team = sqlx::query_as::<_, Team>(
|
||||
"SELECT * FROM teams WHERE id = $1 AND is_delete = false"
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Team not found".to_string()))?;
|
||||
|
||||
let members = get_team_members(&state.pool, team_id).await?;
|
||||
|
||||
// Check user's relationship with team
|
||||
let (is_member, is_captain, pending_request) = if let Some(ref u) = user {
|
||||
let membership = sqlx::query_as::<_, TeamPlayer>(
|
||||
"SELECT * FROM team_players WHERE team_id = $1 AND player_id = $2"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(u.id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
match membership {
|
||||
Some(m) => (
|
||||
m.status == 1,
|
||||
m.status == 1 && m.position == "captain",
|
||||
m.status == 0,
|
||||
),
|
||||
None => (false, false, false),
|
||||
}
|
||||
} else {
|
||||
(false, false, false)
|
||||
};
|
||||
|
||||
Ok(Json(TeamDetailResponse {
|
||||
team: team.into(),
|
||||
members,
|
||||
is_member,
|
||||
is_captain,
|
||||
pending_request,
|
||||
}))
|
||||
}
|
||||
|
||||
/// PUT /api/teams/:id
|
||||
/// Update team (captain only)
|
||||
pub async fn update_team(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(team_id): Path<i32>,
|
||||
Json(payload): Json<UpdateTeamRequest>,
|
||||
) -> Result<Json<TeamResponse>> {
|
||||
payload.validate()?;
|
||||
|
||||
// Check if user is captain
|
||||
let is_captain = check_is_captain(&state.pool, team_id, user.id).await?;
|
||||
if !is_captain {
|
||||
return Err(AppError::Forbidden("Only the captain can update the team".to_string()));
|
||||
}
|
||||
|
||||
// Check if new name conflicts
|
||||
if let Some(ref name) = payload.name {
|
||||
let existing = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM teams WHERE name = $1 AND id != $2 AND is_delete = false"
|
||||
)
|
||||
.bind(name)
|
||||
.bind(team_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if existing.is_some() {
|
||||
return Err(AppError::Conflict("Team name already exists".to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let team = sqlx::query_as::<_, Team>(
|
||||
r#"
|
||||
UPDATE teams
|
||||
SET name = COALESCE($1, name),
|
||||
bio = COALESCE($2, bio)
|
||||
WHERE id = $3 AND is_delete = false
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.bio)
|
||||
.bind(team_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Team not found".to_string()))?;
|
||||
|
||||
Ok(Json(team.into()))
|
||||
}
|
||||
|
||||
/// DELETE /api/teams/:id
|
||||
/// Delete team (captain only)
|
||||
pub async fn delete_team(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(team_id): Path<i32>,
|
||||
) -> Result<StatusCode> {
|
||||
// Check if user is captain
|
||||
let is_captain = check_is_captain(&state.pool, team_id, user.id).await?;
|
||||
if !is_captain && !user.is_admin {
|
||||
return Err(AppError::Forbidden("Only the captain can delete the team".to_string()));
|
||||
}
|
||||
|
||||
// Soft delete team
|
||||
let result = sqlx::query(
|
||||
"UPDATE teams SET is_delete = true WHERE id = $1"
|
||||
)
|
||||
.bind(team_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("Team not found".to_string()));
|
||||
}
|
||||
|
||||
// Remove all team members
|
||||
sqlx::query("DELETE FROM team_players WHERE team_id = $1")
|
||||
.bind(team_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Remove from ladders
|
||||
sqlx::query("DELETE FROM ladder_teams WHERE team_id = $1")
|
||||
.bind(team_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// POST /api/teams/:id/join-request
|
||||
/// Request to join a team
|
||||
pub async fn request_join_team(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(team_id): Path<i32>,
|
||||
) -> Result<StatusCode> {
|
||||
// Check team exists
|
||||
let team_exists = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM teams WHERE id = $1 AND is_delete = false"
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if team_exists.is_none() {
|
||||
return Err(AppError::NotFound("Team not found".to_string()));
|
||||
}
|
||||
|
||||
// Check if user is already in a team (active)
|
||||
let existing_active = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM team_players WHERE player_id = $1 AND status = 1"
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if existing_active.is_some() {
|
||||
return Err(AppError::Conflict("You are already a member of a team".to_string()));
|
||||
}
|
||||
|
||||
// Check if already requested
|
||||
let existing_request = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM team_players WHERE team_id = $1 AND player_id = $2"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
if existing_request.is_some() {
|
||||
return Err(AppError::Conflict("You already have a pending request".to_string()));
|
||||
}
|
||||
|
||||
// Create join request
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO team_players (team_id, player_id, position, status)
|
||||
VALUES ($1, $2, 'member', 0)
|
||||
"#
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(user.id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::CREATED)
|
||||
}
|
||||
|
||||
/// DELETE /api/teams/:id/join-request
|
||||
/// Cancel join request
|
||||
pub async fn cancel_join_request(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(team_id): Path<i32>,
|
||||
) -> Result<StatusCode> {
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM team_players WHERE team_id = $1 AND player_id = $2 AND status = 0"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(user.id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("No pending request found".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// POST /api/teams/:id/members/:player_id/accept
|
||||
/// Accept a join request (captain only)
|
||||
pub async fn accept_member(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path((team_id, player_id)): Path<(i32, i32)>,
|
||||
) -> Result<StatusCode> {
|
||||
// Check if user is captain
|
||||
let is_captain = check_is_captain(&state.pool, team_id, user.id).await?;
|
||||
if !is_captain {
|
||||
return Err(AppError::Forbidden("Only the captain can accept members".to_string()));
|
||||
}
|
||||
|
||||
// Update status to active
|
||||
let result = sqlx::query(
|
||||
"UPDATE team_players SET status = 1 WHERE team_id = $1 AND player_id = $2 AND status = 0"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("No pending request found".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// POST /api/teams/:id/members/:player_id/reject
|
||||
/// Reject a join request (captain only)
|
||||
pub async fn reject_member(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path((team_id, player_id)): Path<(i32, i32)>,
|
||||
) -> Result<StatusCode> {
|
||||
// Check if user is captain
|
||||
let is_captain = check_is_captain(&state.pool, team_id, user.id).await?;
|
||||
if !is_captain {
|
||||
return Err(AppError::Forbidden("Only the captain can reject members".to_string()));
|
||||
}
|
||||
|
||||
let result = sqlx::query(
|
||||
"DELETE FROM team_players WHERE team_id = $1 AND player_id = $2 AND status = 0"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("No pending request found".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// DELETE /api/teams/:id/members/:player_id
|
||||
/// Remove a member or leave team
|
||||
pub async fn remove_member(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path((team_id, player_id)): Path<(i32, i32)>,
|
||||
) -> Result<StatusCode> {
|
||||
let is_captain = check_is_captain(&state.pool, team_id, user.id).await?;
|
||||
let is_self = user.id == player_id;
|
||||
|
||||
// Check permission
|
||||
if !is_captain && !is_self && !user.is_admin {
|
||||
return Err(AppError::Forbidden("You can only remove yourself or be captain to remove others".to_string()));
|
||||
}
|
||||
|
||||
// Check if target is captain
|
||||
let target_membership = sqlx::query_as::<_, TeamPlayer>(
|
||||
"SELECT * FROM team_players WHERE team_id = $1 AND player_id = $2 AND status = 1"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(player_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::NotFound("Member not found".to_string()))?;
|
||||
|
||||
// Captain can't leave without transferring or deleting team
|
||||
if target_membership.position == "captain" && is_self {
|
||||
return Err(AppError::BadRequest(
|
||||
"Captain cannot leave. Transfer captaincy or delete the team.".to_string()
|
||||
));
|
||||
}
|
||||
|
||||
// Remove member
|
||||
sqlx::query(
|
||||
"DELETE FROM team_players WHERE team_id = $1 AND player_id = $2"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
/// PUT /api/teams/:id/members/:player_id/position
|
||||
/// Change member position (captain only)
|
||||
pub async fn change_member_position(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path((team_id, player_id)): Path<(i32, i32)>,
|
||||
Json(payload): Json<ChangePositionRequest>,
|
||||
) -> Result<StatusCode> {
|
||||
// Validate position
|
||||
let position = PlayerPosition::from_str(&payload.position)
|
||||
.ok_or_else(|| AppError::BadRequest("Invalid position. Use: captain, co-captain, or member".to_string()))?;
|
||||
|
||||
// Check if user is captain
|
||||
let is_captain = check_is_captain(&state.pool, team_id, user.id).await?;
|
||||
if !is_captain {
|
||||
return Err(AppError::Forbidden("Only the captain can change positions".to_string()));
|
||||
}
|
||||
|
||||
// If promoting to captain, demote current captain
|
||||
if position == PlayerPosition::Captain && player_id != user.id {
|
||||
// Demote current captain to co-captain
|
||||
sqlx::query(
|
||||
"UPDATE team_players SET position = 'co-captain' WHERE team_id = $1 AND player_id = $2"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(user.id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Update position
|
||||
let result = sqlx::query(
|
||||
"UPDATE team_players SET position = $1 WHERE team_id = $2 AND player_id = $3 AND status = 1"
|
||||
)
|
||||
.bind(position.as_str())
|
||||
.bind(team_id)
|
||||
.bind(player_id)
|
||||
.execute(&state.pool)
|
||||
.await?;
|
||||
|
||||
if result.rows_affected() == 0 {
|
||||
return Err(AppError::NotFound("Member not found".to_string()));
|
||||
}
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
/// GET /api/my-team
|
||||
/// Get current user's team
|
||||
pub async fn get_my_team(
|
||||
user: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<MyTeamResponse>> {
|
||||
// Get user's team membership
|
||||
let membership = sqlx::query_as::<_, TeamPlayer>(
|
||||
"SELECT * FROM team_players WHERE player_id = $1 AND status = 1"
|
||||
)
|
||||
.bind(user.id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await?;
|
||||
|
||||
let Some(membership) = membership else {
|
||||
return Ok(Json(MyTeamResponse {
|
||||
team: None,
|
||||
position: None,
|
||||
members: vec![],
|
||||
pending_requests: vec![],
|
||||
}));
|
||||
};
|
||||
|
||||
// Get team
|
||||
let team = sqlx::query_as::<_, Team>(
|
||||
"SELECT * FROM teams WHERE id = $1 AND is_delete = false"
|
||||
)
|
||||
.bind(membership.team_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await?;
|
||||
|
||||
// Get members
|
||||
let members = get_team_members(&state.pool, membership.team_id).await?;
|
||||
|
||||
// Get pending requests (if captain)
|
||||
let pending_requests = if membership.position == "captain" {
|
||||
sqlx::query_as::<_, TeamMemberResponse>(
|
||||
r#"
|
||||
SELECT tp.id, tp.player_id, u.username, u.firstname, u.lastname, u.profile,
|
||||
tp.position, tp.status, tp.date_created as date_joined
|
||||
FROM team_players tp
|
||||
JOIN users u ON tp.player_id = u.id
|
||||
WHERE tp.team_id = $1 AND tp.status = 0
|
||||
ORDER BY tp.date_created ASC
|
||||
"#
|
||||
)
|
||||
.bind(membership.team_id)
|
||||
.fetch_all(&state.pool)
|
||||
.await?
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Ok(Json(MyTeamResponse {
|
||||
team: Some(team.into()),
|
||||
position: Some(membership.position),
|
||||
members,
|
||||
pending_requests,
|
||||
}))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
async fn get_team_members(pool: &sqlx::PgPool, team_id: i32) -> Result<Vec<TeamMemberResponse>> {
|
||||
let members = sqlx::query_as::<_, TeamMemberResponse>(
|
||||
r#"
|
||||
SELECT tp.id, tp.player_id, u.username, u.firstname, u.lastname, u.profile,
|
||||
tp.position, tp.status, tp.date_created as date_joined
|
||||
FROM team_players tp
|
||||
JOIN users u ON tp.player_id = u.id
|
||||
WHERE tp.team_id = $1 AND tp.status = 1
|
||||
ORDER BY
|
||||
CASE tp.position
|
||||
WHEN 'captain' THEN 1
|
||||
WHEN 'co-captain' THEN 2
|
||||
ELSE 3
|
||||
END,
|
||||
tp.date_created ASC
|
||||
"#
|
||||
)
|
||||
.bind(team_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
Ok(members)
|
||||
}
|
||||
|
||||
async fn check_is_captain(pool: &sqlx::PgPool, team_id: i32, user_id: i32) -> Result<bool> {
|
||||
let is_captain = sqlx::query_scalar::<_, i32>(
|
||||
"SELECT id FROM team_players WHERE team_id = $1 AND player_id = $2 AND position = 'captain' AND status = 1"
|
||||
)
|
||||
.bind(team_id)
|
||||
.bind(user_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
|
||||
Ok(is_captain.is_some())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Logo Upload
|
||||
// Note: Team logo upload is now handled by uploads.rs using multipart forms
|
||||
Reference in New Issue
Block a user