426 lines
15 KiB
Python
426 lines
15 KiB
Python
"""
|
|
Tenant Member Management API - ATOMIC operations
|
|
Handles team member CRUD operations
|
|
"""
|
|
|
|
import structlog
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
|
|
from typing import List, Dict, Any
|
|
from uuid import UUID
|
|
|
|
from app.schemas.tenants import TenantMemberResponse, AddMemberWithUserCreate, TenantResponse
|
|
from app.services.tenant_service import EnhancedTenantService
|
|
from shared.auth.decorators import get_current_user_dep
|
|
from shared.routing.route_builder import RouteBuilder
|
|
from shared.database.base import create_database_manager
|
|
from shared.monitoring.metrics import track_endpoint_metrics
|
|
|
|
logger = structlog.get_logger()
|
|
router = APIRouter()
|
|
route_builder = RouteBuilder("tenants")
|
|
|
|
# Dependency injection for enhanced tenant service
|
|
def get_enhanced_tenant_service():
|
|
try:
|
|
from app.core.config import settings
|
|
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
|
|
return EnhancedTenantService(database_manager)
|
|
except Exception as e:
|
|
logger.error("Failed to create enhanced tenant service", error=str(e))
|
|
raise HTTPException(status_code=500, detail="Service initialization failed")
|
|
|
|
@router.post(route_builder.build_base_route("{tenant_id}/members/with-user", include_tenant_prefix=False), response_model=TenantMemberResponse)
|
|
@track_endpoint_metrics("tenant_add_member_with_user_creation")
|
|
async def add_team_member_with_user_creation(
|
|
member_data: AddMemberWithUserCreate,
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""
|
|
Add a team member to tenant with optional user creation (pilot phase).
|
|
|
|
This endpoint supports two modes:
|
|
1. Adding an existing user: Set user_id and create_user=False
|
|
2. Creating a new user: Set create_user=True and provide email, full_name, password
|
|
|
|
In pilot phase, this allows owners to directly create users with passwords.
|
|
In production, this will be replaced with an invitation-based flow.
|
|
"""
|
|
try:
|
|
user_id_to_add = member_data.user_id
|
|
|
|
# If create_user is True, create the user first via auth service
|
|
if member_data.create_user:
|
|
logger.info(
|
|
"Creating new user before adding to tenant",
|
|
tenant_id=str(tenant_id),
|
|
email=member_data.email,
|
|
requested_by=current_user["user_id"]
|
|
)
|
|
|
|
# Call auth service to create user
|
|
from shared.clients.auth_client import AuthServiceClient
|
|
from app.core.config import settings
|
|
|
|
auth_client = AuthServiceClient(settings)
|
|
|
|
# Map tenant role to user role
|
|
# tenant roles: admin, member, viewer
|
|
# user roles: admin, manager, user
|
|
user_role_map = {
|
|
"admin": "admin",
|
|
"member": "manager",
|
|
"viewer": "user"
|
|
}
|
|
user_role = user_role_map.get(member_data.role, "user")
|
|
|
|
try:
|
|
user_create_data = {
|
|
"email": member_data.email,
|
|
"full_name": member_data.full_name,
|
|
"password": member_data.password,
|
|
"phone": member_data.phone,
|
|
"role": user_role,
|
|
"language": member_data.language or "es",
|
|
"timezone": member_data.timezone or "Europe/Madrid"
|
|
}
|
|
|
|
created_user = await auth_client.create_user_by_owner(user_create_data)
|
|
user_id_to_add = created_user.get("id")
|
|
|
|
logger.info(
|
|
"User created successfully",
|
|
user_id=user_id_to_add,
|
|
email=member_data.email,
|
|
tenant_id=str(tenant_id)
|
|
)
|
|
|
|
except Exception as auth_error:
|
|
logger.error(
|
|
"Failed to create user via auth service",
|
|
error=str(auth_error),
|
|
email=member_data.email
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail=f"Failed to create user account: {str(auth_error)}"
|
|
)
|
|
|
|
# Add the user (existing or newly created) to the tenant
|
|
result = await tenant_service.add_team_member(
|
|
str(tenant_id),
|
|
user_id_to_add,
|
|
member_data.role,
|
|
current_user["user_id"]
|
|
)
|
|
|
|
logger.info(
|
|
"Team member added successfully",
|
|
tenant_id=str(tenant_id),
|
|
user_id=user_id_to_add,
|
|
role=member_data.role,
|
|
user_was_created=member_data.create_user
|
|
)
|
|
|
|
return result
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(
|
|
"Add team member with user creation failed",
|
|
tenant_id=str(tenant_id),
|
|
error=str(e)
|
|
)
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to add team member"
|
|
)
|
|
|
|
|
|
@router.post(route_builder.build_base_route("{tenant_id}/members", include_tenant_prefix=False), response_model=TenantMemberResponse)
|
|
@track_endpoint_metrics("tenant_add_member")
|
|
async def add_team_member(
|
|
user_id: str,
|
|
role: str,
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""Add an existing team member to tenant (legacy endpoint)"""
|
|
|
|
try:
|
|
result = await tenant_service.add_team_member(
|
|
str(tenant_id),
|
|
user_id,
|
|
role,
|
|
current_user["user_id"]
|
|
)
|
|
return result
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Add team member failed",
|
|
tenant_id=str(tenant_id),
|
|
user_id=user_id,
|
|
role=role,
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to add team member"
|
|
)
|
|
|
|
@router.get(route_builder.build_base_route("{tenant_id}/members", include_tenant_prefix=False), response_model=List[TenantMemberResponse])
|
|
async def get_team_members(
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
active_only: bool = Query(True, description="Only return active members"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""Get all team members for a tenant with enhanced filtering"""
|
|
|
|
try:
|
|
members = await tenant_service.get_team_members(
|
|
str(tenant_id),
|
|
current_user["user_id"],
|
|
active_only=active_only
|
|
)
|
|
return members
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Get team members failed",
|
|
tenant_id=str(tenant_id),
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to get team members"
|
|
)
|
|
|
|
@router.put(route_builder.build_base_route("{tenant_id}/members/{member_user_id}/role", include_tenant_prefix=False), response_model=TenantMemberResponse)
|
|
@track_endpoint_metrics("tenant_update_member_role")
|
|
async def update_member_role(
|
|
new_role: str,
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
member_user_id: str = Path(..., description="Member user ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""Update team member role with enhanced permission validation"""
|
|
|
|
try:
|
|
result = await tenant_service.update_member_role(
|
|
str(tenant_id),
|
|
member_user_id,
|
|
new_role,
|
|
current_user["user_id"]
|
|
)
|
|
return result
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Update member role failed",
|
|
tenant_id=str(tenant_id),
|
|
member_user_id=member_user_id,
|
|
new_role=new_role,
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to update member role"
|
|
)
|
|
|
|
@router.delete(route_builder.build_base_route("{tenant_id}/members/{member_user_id}", include_tenant_prefix=False))
|
|
@track_endpoint_metrics("tenant_remove_member")
|
|
async def remove_team_member(
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
member_user_id: str = Path(..., description="Member user ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""Remove team member from tenant with enhanced validation"""
|
|
|
|
try:
|
|
success = await tenant_service.remove_team_member(
|
|
str(tenant_id),
|
|
member_user_id,
|
|
current_user["user_id"]
|
|
)
|
|
|
|
if success:
|
|
return {"success": True, "message": "Team member removed successfully"}
|
|
else:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to remove team member"
|
|
)
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Remove team member failed",
|
|
tenant_id=str(tenant_id),
|
|
member_user_id=member_user_id,
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to remove team member"
|
|
)
|
|
|
|
@router.delete(route_builder.build_base_route("user/{user_id}/memberships", include_tenant_prefix=False))
|
|
@track_endpoint_metrics("user_memberships_delete")
|
|
async def delete_user_memberships(
|
|
user_id: str = Path(..., description="User ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""
|
|
Delete all tenant memberships for a user.
|
|
Used by auth service when deleting a user account.
|
|
Only accessible by internal services.
|
|
"""
|
|
|
|
logger.info(
|
|
"Delete user memberships request received",
|
|
user_id=user_id,
|
|
requesting_service=current_user.get("service", "unknown"),
|
|
is_service=current_user.get("type") == "service"
|
|
)
|
|
|
|
# Only allow internal service calls
|
|
if current_user.get("type") != "service":
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="This endpoint is only accessible to internal services"
|
|
)
|
|
|
|
try:
|
|
result = await tenant_service.delete_user_memberships(user_id)
|
|
|
|
logger.info(
|
|
"User memberships deleted successfully",
|
|
user_id=user_id,
|
|
deleted_count=result.get("deleted_count"),
|
|
total_memberships=result.get("total_memberships")
|
|
)
|
|
|
|
return {
|
|
"message": "User memberships deleted successfully",
|
|
"summary": result
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Delete user memberships failed",
|
|
user_id=user_id,
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to delete user memberships"
|
|
)
|
|
|
|
@router.post(route_builder.build_base_route("{tenant_id}/transfer-ownership", include_tenant_prefix=False), response_model=TenantResponse)
|
|
@track_endpoint_metrics("tenant_transfer_ownership")
|
|
async def transfer_ownership(
|
|
new_owner_id: str,
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""
|
|
Transfer tenant ownership to another admin.
|
|
Only the current owner or internal services can perform this action.
|
|
"""
|
|
|
|
logger.info(
|
|
"Transfer ownership request received",
|
|
tenant_id=str(tenant_id),
|
|
new_owner_id=new_owner_id,
|
|
requesting_user=current_user.get("user_id"),
|
|
is_service=current_user.get("type") == "service"
|
|
)
|
|
|
|
try:
|
|
# Get current tenant to find current owner
|
|
tenant_info = await tenant_service.get_tenant_by_id(str(tenant_id))
|
|
if not tenant_info:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Tenant not found"
|
|
)
|
|
|
|
current_owner_id = tenant_info.owner_id
|
|
|
|
result = await tenant_service.transfer_tenant_ownership(
|
|
str(tenant_id),
|
|
current_owner_id,
|
|
new_owner_id,
|
|
requesting_user_id=current_user.get("user_id")
|
|
)
|
|
|
|
logger.info(
|
|
"Ownership transferred successfully",
|
|
tenant_id=str(tenant_id),
|
|
from_owner=current_owner_id,
|
|
to_owner=new_owner_id
|
|
)
|
|
|
|
return result
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Transfer ownership failed",
|
|
tenant_id=str(tenant_id),
|
|
new_owner_id=new_owner_id,
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to transfer ownership"
|
|
)
|
|
|
|
@router.get(route_builder.build_base_route("{tenant_id}/admins", include_tenant_prefix=False), response_model=List[TenantMemberResponse])
|
|
@track_endpoint_metrics("tenant_get_admins")
|
|
async def get_tenant_admins(
|
|
tenant_id: UUID = Path(..., description="Tenant ID"),
|
|
current_user: Dict[str, Any] = Depends(get_current_user_dep),
|
|
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
|
|
):
|
|
"""
|
|
Get all admins (owner + admins) for a tenant.
|
|
Used by auth service to check for other admins before tenant deletion.
|
|
"""
|
|
|
|
logger.info(
|
|
"Get tenant admins request received",
|
|
tenant_id=str(tenant_id),
|
|
requesting_user=current_user.get("user_id"),
|
|
is_service=current_user.get("type") == "service"
|
|
)
|
|
|
|
try:
|
|
admins = await tenant_service.get_tenant_admins(str(tenant_id))
|
|
|
|
logger.info(
|
|
"Retrieved tenant admins",
|
|
tenant_id=str(tenant_id),
|
|
admin_count=len(admins)
|
|
)
|
|
|
|
return admins
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error("Get tenant admins failed",
|
|
tenant_id=str(tenant_id),
|
|
error=str(e))
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Failed to get tenant admins"
|
|
)
|