use crate::core::models::user::types::{User, UserRole};
use crate::core::teams::{
AddMemberRequest, CreateTeamRequest, Team, TeamManager, TeamRole, TeamStatus,
UpdateRoleRequest, UpdateTeamRequest,
};
use crate::core::types::context::{RequestContext, SharedRequestContext};
use crate::server::routes::{ApiResponse, PaginatedResponse, PaginationQuery, errors};
use crate::server::state::AppState;
use crate::utils::error::gateway_error::GatewayError;
use actix_web::{HttpMessage, HttpRequest, HttpResponse, Result as ActixResult, web};
use std::sync::Arc;
use tracing::{error, info};
use uuid::Uuid;
#[derive(Debug, Clone, serde::Deserialize)]
pub struct CreateTeamBody {
pub name: String,
pub display_name: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct UpdateTeamBody {
pub name: Option<String>,
pub display_name: Option<String>,
pub description: Option<String>,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct AddMemberBody {
pub user_id: Uuid,
pub role: TeamRole,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct UpdateRoleBody {
pub role: TeamRole,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct TeamResponse {
#[serde(flatten)]
pub team: Team,
#[serde(skip_serializing_if = "Option::is_none")]
pub member_count: Option<usize>,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct TeamPath {
pub id: Uuid,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct MemberPath {
pub id: Uuid,
pub user_id: Uuid,
}
fn get_team_manager(state: &web::Data<AppState>) -> Arc<TeamManager> {
state.team_manager.clone()
}
#[derive(Debug, Clone)]
enum RequestCaller {
User(Box<User>),
Team(Uuid),
}
#[derive(Debug, Clone, Copy)]
enum TeamPermission {
Member,
Admin,
}
fn is_auth_enabled(state: &web::Data<AppState>) -> bool {
let cfg = state.config.load();
cfg.auth().enable_jwt || cfg.auth().enable_api_key
}
fn unauthorized_response(message: &str) -> HttpResponse {
HttpResponse::Unauthorized().json(ApiResponse::<()>::error(message.to_string()))
}
fn forbidden_response(message: &str) -> HttpResponse {
HttpResponse::Forbidden().json(ApiResponse::<()>::error(message.to_string()))
}
fn get_request_caller(req: &HttpRequest) -> Option<RequestCaller> {
if let Some(user) = req.extensions().get::<User>() {
return Some(RequestCaller::User(Box::new(user.clone())));
}
if let Some(ctx) = req.extensions().get::<SharedRequestContext>() {
return ctx.team_id().map(RequestCaller::Team);
}
req.extensions()
.get::<RequestContext>()
.and_then(|ctx| ctx.team_id())
.map(RequestCaller::Team)
}
async fn has_team_access(
manager: &TeamManager,
caller: &RequestCaller,
team_id: Uuid,
required: TeamPermission,
) -> Result<bool, GatewayError> {
match caller {
RequestCaller::User(user) => {
if user.has_role(&UserRole::Admin) {
return Ok(true);
}
match required {
TeamPermission::Member => {
manager
.check_user_role(
team_id,
user.id(),
&[
TeamRole::Owner,
TeamRole::Admin,
TeamRole::Manager,
TeamRole::Member,
TeamRole::Viewer,
],
)
.await
}
TeamPermission::Admin => manager.is_team_admin(team_id, user.id()).await,
}
}
RequestCaller::Team(caller_team_id) => {
Ok(*caller_team_id == team_id && matches!(required, TeamPermission::Member))
}
}
}
async fn authorize_team_operation(
req: &HttpRequest,
state: &web::Data<AppState>,
team_id: Uuid,
required: TeamPermission,
) -> Result<(), HttpResponse> {
if !is_auth_enabled(state) {
return Ok(());
}
let caller = match get_request_caller(req) {
Some(caller) => caller,
None => return Err(unauthorized_response("Authentication required")),
};
let manager = get_team_manager(state);
match has_team_access(&manager, &caller, team_id, required).await {
Ok(true) => Ok(()),
Ok(false) => {
if matches!(&caller, RequestCaller::Team(_))
&& matches!(required, TeamPermission::Admin)
{
Err(forbidden_response(
"Team-scoped API keys cannot perform this operation",
))
} else {
Err(forbidden_response("Not authorized for this team operation"))
}
}
Err(e) => Err(errors::gateway_error_to_response(e)),
}
}
fn resolve_invited_by(req: &HttpRequest) -> Option<Uuid> {
if let Some(user) = req.extensions().get::<User>() {
return Some(user.id());
}
if let Some(ctx) = req.extensions().get::<SharedRequestContext>() {
return ctx
.user_id
.as_deref()
.and_then(|user_id| Uuid::parse_str(user_id).ok());
}
req.extensions()
.get::<RequestContext>()
.and_then(|ctx| ctx.user_id.as_deref())
.and_then(|user_id| Uuid::parse_str(user_id).ok())
}
pub async fn create_team(
req: HttpRequest,
state: web::Data<AppState>,
payload: web::Json<CreateTeamBody>,
) -> ActixResult<HttpResponse> {
info!("Creating team: {}", payload.name);
if is_auth_enabled(&state) {
match get_request_caller(&req) {
Some(RequestCaller::User(user)) if user.has_role(&UserRole::Admin) => {}
Some(_) => {
return Ok(forbidden_response(
"Admin privileges required to create teams",
));
}
None => return Ok(unauthorized_response("Authentication required")),
}
}
let manager = get_team_manager(&state);
let request = CreateTeamRequest {
name: payload.name.clone(),
display_name: payload.display_name.clone(),
description: payload.description.clone(),
settings: None,
};
match manager.create_team(request).await {
Ok(team) => {
info!("Team created: {} ({})", team.name, team.id());
Ok(
HttpResponse::Created().json(ApiResponse::success(TeamResponse {
team,
member_count: Some(0),
})),
)
}
Err(e) => {
error!("Failed to create team: {}", e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn list_teams(
req: HttpRequest,
state: web::Data<AppState>,
query: web::Query<PaginationQuery>,
) -> ActixResult<HttpResponse> {
if let Err(e) = query.validate() {
return Ok(errors::validation_error(&e));
}
let manager = get_team_manager(&state);
let list_result = if !is_auth_enabled(&state) {
manager.list_teams(query.offset(), query.limit).await
} else {
match get_request_caller(&req) {
Some(RequestCaller::User(user)) if user.has_role(&UserRole::Admin) => {
manager.list_teams(query.offset(), query.limit).await
}
Some(RequestCaller::User(user)) => match manager.get_user_teams(user.id()).await {
Ok(teams) => {
let visible: Vec<Team> = teams
.into_iter()
.filter(|t| !matches!(t.status, TeamStatus::Deleted))
.collect();
let total = visible.len() as u64;
let paginated = visible
.into_iter()
.skip(query.offset() as usize)
.take(query.limit as usize)
.collect();
Ok((paginated, total))
}
Err(e) => Err(e),
},
Some(RequestCaller::Team(team_id)) => match manager.get_team(team_id).await {
Ok(team) if !matches!(team.status, TeamStatus::Deleted) => {
let mut teams = vec![team];
let total = teams.len() as u64;
let paginated = teams
.drain(..)
.skip(query.offset() as usize)
.take(query.limit as usize)
.collect();
Ok((paginated, total))
}
Ok(_) => Ok((Vec::new(), 0)),
Err(GatewayError::NotFound(_)) => Ok((Vec::new(), 0)),
Err(e) => Err(e),
},
None => return Ok(unauthorized_response("Authentication required")),
}
};
match list_result {
Ok((teams, total)) => {
let team_responses: Vec<TeamResponse> = teams
.into_iter()
.map(|team| TeamResponse {
team,
member_count: None,
})
.collect();
Ok(
HttpResponse::Ok().json(ApiResponse::success(PaginatedResponse::new(
team_responses,
query.page,
query.limit,
total,
))),
)
}
Err(e) => {
error!("Failed to list teams: {}", e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn get_team(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<TeamPath>,
) -> ActixResult<HttpResponse> {
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Member).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
match manager.get_team(path.id).await {
Ok(team) => {
let member_count = manager
.list_members(path.id)
.await
.map(|m| m.len())
.unwrap_or(0);
Ok(HttpResponse::Ok().json(ApiResponse::success(TeamResponse {
team,
member_count: Some(member_count),
})))
}
Err(e) => {
error!("Failed to get team {}: {}", path.id, e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn update_team(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<TeamPath>,
body: web::Json<UpdateTeamBody>,
) -> ActixResult<HttpResponse> {
info!("Updating team: {}", path.id);
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Admin).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
let request = UpdateTeamRequest {
name: body.name.clone(),
display_name: body.display_name.clone(),
description: body.description.clone(),
settings: None,
status: None,
};
match manager.update_team(path.id, request).await {
Ok(team) => {
info!("Team updated: {} ({})", team.name, team.id());
Ok(HttpResponse::Ok().json(ApiResponse::success(TeamResponse {
team,
member_count: None,
})))
}
Err(e) => {
error!("Failed to update team {}: {}", path.id, e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn delete_team(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<TeamPath>,
) -> ActixResult<HttpResponse> {
info!("Deleting team: {}", path.id);
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Admin).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
match manager.delete_team(path.id).await {
Ok(()) => {
info!("Team deleted: {}", path.id);
Ok(HttpResponse::NoContent().finish())
}
Err(e) => {
error!("Failed to delete team {}: {}", path.id, e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn add_member(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<TeamPath>,
body: web::Json<AddMemberBody>,
) -> ActixResult<HttpResponse> {
info!(
"Adding member {} to team {} with role {:?}",
body.user_id, path.id, body.role
);
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Admin).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
let request = AddMemberRequest {
user_id: body.user_id,
role: body.role.clone(),
};
let invited_by = resolve_invited_by(&req);
match manager.add_member(path.id, request, invited_by).await {
Ok(member) => {
info!("Member {} added to team {}", member.user_id, path.id);
Ok(HttpResponse::Created().json(ApiResponse::success(member)))
}
Err(e) => {
error!(
"Failed to add member {} to team {}: {}",
body.user_id, path.id, e
);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn list_members(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<TeamPath>,
) -> ActixResult<HttpResponse> {
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Member).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
match manager.list_members(path.id).await {
Ok(members) => Ok(HttpResponse::Ok().json(ApiResponse::success(members))),
Err(e) => {
error!("Failed to list members for team {}: {}", path.id, e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn remove_member(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<MemberPath>,
) -> ActixResult<HttpResponse> {
info!("Removing member {} from team {}", path.user_id, path.id);
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Admin).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
match manager.remove_member(path.id, path.user_id).await {
Ok(()) => {
info!("Member {} removed from team {}", path.user_id, path.id);
Ok(HttpResponse::NoContent().finish())
}
Err(e) => {
error!(
"Failed to remove member {} from team {}: {}",
path.user_id, path.id, e
);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn update_member_role(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<MemberPath>,
body: web::Json<UpdateRoleBody>,
) -> ActixResult<HttpResponse> {
info!(
"Updating role for member {} in team {} to {:?}",
path.user_id, path.id, body.role
);
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Admin).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
let request = UpdateRoleRequest {
role: body.role.clone(),
};
match manager
.update_member_role(path.id, path.user_id, request)
.await
{
Ok(member) => {
info!(
"Member {} role updated to {:?} in team {}",
path.user_id, member.role, path.id
);
Ok(HttpResponse::Ok().json(ApiResponse::success(member)))
}
Err(e) => {
error!(
"Failed to update role for member {} in team {}: {}",
path.user_id, path.id, e
);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub async fn get_team_usage(
req: HttpRequest,
state: web::Data<AppState>,
path: web::Path<TeamPath>,
) -> ActixResult<HttpResponse> {
if let Err(resp) = authorize_team_operation(&req, &state, path.id, TeamPermission::Member).await
{
return Ok(resp);
}
let manager = get_team_manager(&state);
match manager.get_team_usage(path.id).await {
Ok(usage) => Ok(HttpResponse::Ok().json(ApiResponse::success(usage))),
Err(e) => {
error!("Failed to get usage for team {}: {}", path.id, e);
Ok(errors::gateway_error_to_response(e))
}
}
}
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/v1/teams")
.route("", web::post().to(create_team))
.route("", web::get().to(list_teams))
.route("/{id}", web::get().to(get_team))
.route("/{id}", web::put().to(update_team))
.route("/{id}", web::delete().to(delete_team))
.route("/{id}/members", web::post().to(add_member))
.route("/{id}/members", web::get().to(list_members))
.route("/{id}/members/{user_id}", web::delete().to(remove_member))
.route(
"/{id}/members/{user_id}/role",
web::put().to(update_member_role),
)
.route("/{id}/usage", web::get().to(get_team_usage)),
);
}
#[cfg(test)]
#[path = "teams_tests.rs"]
mod tests;