Files
bakery-ia/services/tenant/app/api/tenant_members.py

484 lines
17 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:
# CRITICAL: Check subscription limit before adding user
from app.services.subscription_limit_service import SubscriptionLimitService
limit_service = SubscriptionLimitService()
limit_check = await limit_service.can_add_user(str(tenant_id))
if not limit_check.get('can_add', False):
logger.warning(
"User limit exceeded",
tenant_id=str(tenant_id),
current=limit_check.get('current_count'),
max=limit_check.get('max_allowed'),
reason=limit_check.get('reason')
)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"error": "user_limit_exceeded",
"message": limit_check.get('reason', 'User limit exceeded'),
"current_count": limit_check.get('current_count'),
"max_allowed": limit_check.get('max_allowed'),
"upgrade_required": True
}
)
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:
# CRITICAL: Check subscription limit before adding user
from app.services.subscription_limit_service import SubscriptionLimitService
limit_service = SubscriptionLimitService()
limit_check = await limit_service.can_add_user(str(tenant_id))
if not limit_check.get('can_add', False):
logger.warning(
"User limit exceeded",
tenant_id=str(tenant_id),
current=limit_check.get('current_count'),
max=limit_check.get('max_allowed'),
reason=limit_check.get('reason')
)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
detail={
"error": "user_limit_exceeded",
"message": limit_check.get('reason', 'User limit exceeded'),
"current_count": limit_check.get('current_count'),
"max_allowed": limit_check.get('max_allowed'),
"upgrade_required": True
}
)
result = await tenant_service.add_team_member(
str(tenant_id),
user_id,
role,
current_user["user_id"]
)
logger.info(
"Team member added successfully",
tenant_id=str(tenant_id),
user_id=user_id,
role=role
)
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"
)