REFACTOR - Database logic

This commit is contained in:
Urtzi Alfaro
2025-08-08 09:08:41 +02:00
parent 0154365bfc
commit 488bb3ef93
113 changed files with 22842 additions and 6503 deletions

View File

@@ -0,0 +1,8 @@
"""
Tenant API Package
API endpoints for tenant management
"""
from . import tenants
__all__ = ["tenants"]

View File

@@ -1,59 +1,75 @@
# services/tenant/app/api/tenants.py
"""
Tenant API endpoints
Enhanced Tenant API endpoints using repository pattern and dependency injection
"""
from fastapi import APIRouter, Depends, HTTPException, status, Path
from sqlalchemy.ext.asyncio import AsyncSession
from typing import List, Dict, Any
import structlog
from uuid import UUID
from sqlalchemy import select, delete, func
from datetime import datetime
import uuid
from fastapi import APIRouter, Depends, HTTPException, status, Path, Query
from typing import List, Dict, Any, Optional
from uuid import UUID
from app.core.database import get_db
from app.services.messaging import publish_tenant_deleted_event
from app.schemas.tenants import (
BakeryRegistration, TenantResponse, TenantAccessResponse,
TenantUpdate, TenantMemberResponse
TenantUpdate, TenantMemberResponse, TenantSearchRequest
)
from app.services.tenant_service import TenantService
from app.services.tenant_service import EnhancedTenantService
from shared.auth.decorators import (
get_current_user_dep,
require_admin_role,
require_admin_role_dep
)
from shared.database.base import create_database_manager
from shared.monitoring.metrics import track_endpoint_metrics
logger = structlog.get_logger()
router = APIRouter()
# Dependency injection for enhanced tenant service
def get_enhanced_tenant_service():
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
return EnhancedTenantService(database_manager)
@router.post("/tenants/register", response_model=TenantResponse)
async def register_bakery(
async def register_bakery_enhanced(
bakery_data: BakeryRegistration,
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Register a new bakery/tenant with enhanced validation and features"""
try:
result = await TenantService.create_bakery(bakery_data, current_user["user_id"], db)
logger.info(f"Bakery registered: {bakery_data.name} by {current_user['email']}")
result = await tenant_service.create_bakery(
bakery_data,
current_user["user_id"]
)
logger.info("Bakery registered successfully",
name=bakery_data.name,
owner_email=current_user.get('email'),
tenant_id=result.id)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Bakery registration failed: {e}")
logger.error("Bakery registration failed",
name=bakery_data.name,
owner_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Bakery registration failed"
)
@router.get("/tenants/{tenant_id}/access/{user_id}", response_model=TenantAccessResponse)
async def verify_tenant_access(
user_id: str,
async def verify_tenant_access_enhanced(
tenant_id: UUID = Path(..., description="Tenant ID"),
db: AsyncSession = Depends(get_db)
user_id: str = Path(..., description="User ID")
):
"""Verify if user has access to tenant - Called by Gateway"""
"""Verify if user has access to tenant - Enhanced version with detailed permissions"""
# Check if this is a service request
if user_id in ["training-service", "data-service", "forecasting-service", "auth-service"]:
# Services have access to all tenants for their operations
@@ -64,32 +80,42 @@ async def verify_tenant_access(
)
try:
access_info = await TenantService.verify_user_access(user_id, tenant_id, db)
# Create tenant service directly
from app.core.config import settings
database_manager = create_database_manager(settings.DATABASE_URL, "tenant-service")
tenant_service = EnhancedTenantService(database_manager)
access_info = await tenant_service.verify_user_access(user_id, str(tenant_id))
return access_info
except Exception as e:
logger.error(f"Access verification failed: {e}")
logger.error("Access verification failed",
user_id=user_id,
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Access verification failed"
)
@router.get("/tenants/{tenant_id}", response_model=TenantResponse)
async def get_tenant(
@track_endpoint_metrics("tenant_get")
async def get_tenant_enhanced(
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get tenant by ID with enhanced data and access control"""
# Verify user has access to tenant
access = await TenantService.verify_user_access(current_user["user_id"], tenant_id, db)
access = await tenant_service.verify_user_access(current_user["user_id"], str(tenant_id))
if not access.has_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant"
)
tenant = await TenantService.get_tenant_by_id(tenant_id, db)
tenant = await tenant_service.get_tenant_by_id(str(tenant_id))
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -98,481 +124,371 @@ async def get_tenant(
return tenant
@router.get("/tenants/subdomain/{subdomain}", response_model=TenantResponse)
@track_endpoint_metrics("tenant_get_by_subdomain")
async def get_tenant_by_subdomain_enhanced(
subdomain: str = Path(..., description="Tenant subdomain"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get tenant by subdomain with enhanced validation"""
tenant = await tenant_service.get_tenant_by_subdomain(subdomain)
if not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Tenant not found"
)
# Verify user has access to this tenant
access = await tenant_service.verify_user_access(current_user["user_id"], tenant.id)
if not access.has_access:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied to tenant"
)
return tenant
@router.get("/tenants/user/{user_id}/owned", response_model=List[TenantResponse])
@track_endpoint_metrics("tenant_get_user_owned")
async def get_user_owned_tenants_enhanced(
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)
):
"""Get all tenants owned by a user with enhanced data"""
# Users can only get their own tenants unless they're admin
user_role = current_user.get('role', '').lower()
if user_id != current_user["user_id"] and user_role != 'admin':
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Can only access your own tenants"
)
tenants = await tenant_service.get_user_tenants(user_id)
return tenants
@router.get("/tenants/search", response_model=List[TenantResponse])
@track_endpoint_metrics("tenant_search")
async def search_tenants_enhanced(
search_term: str = Query(..., description="Search term"),
business_type: Optional[str] = Query(None, description="Business type filter"),
city: Optional[str] = Query(None, description="City filter"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(50, ge=1, le=100, description="Maximum number of records to return"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Search tenants with advanced filters and pagination"""
tenants = await tenant_service.search_tenants(
search_term=search_term,
business_type=business_type,
city=city,
skip=skip,
limit=limit
)
return tenants
@router.get("/tenants/nearby", response_model=List[TenantResponse])
@track_endpoint_metrics("tenant_get_nearby")
async def get_nearby_tenants_enhanced(
latitude: float = Query(..., description="Latitude coordinate"),
longitude: float = Query(..., description="Longitude coordinate"),
radius_km: float = Query(10.0, ge=0.1, le=100.0, description="Search radius in kilometers"),
limit: int = Query(50, ge=1, le=100, description="Maximum number of results"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get tenants near a geographic location with enhanced geospatial search"""
tenants = await tenant_service.get_tenants_near_location(
latitude=latitude,
longitude=longitude,
radius_km=radius_km,
limit=limit
)
return tenants
@router.put("/tenants/{tenant_id}", response_model=TenantResponse)
async def update_tenant(
@track_endpoint_metrics("tenant_update")
async def update_tenant_enhanced(
update_data: TenantUpdate,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Update tenant information with enhanced validation and permission checks"""
try:
result = await TenantService.update_tenant(tenant_id, update_data, current_user["user_id"], db)
result = await tenant_service.update_tenant(
str(tenant_id),
update_data,
current_user["user_id"]
)
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"Tenant update failed: {e}")
logger.error("Tenant update failed",
tenant_id=str(tenant_id),
user_id=current_user["user_id"],
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Tenant update failed"
)
@router.put("/tenants/{tenant_id}/model-status")
@track_endpoint_metrics("tenant_update_model_status")
async def update_tenant_model_status_enhanced(
tenant_id: UUID = Path(..., description="Tenant ID"),
model_trained: bool = Query(..., description="Whether model is trained"),
last_training_date: Optional[datetime] = Query(None, description="Last training date"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Update tenant model training status with enhanced tracking"""
try:
result = await tenant_service.update_model_status(
str(tenant_id),
model_trained,
current_user["user_id"],
last_training_date
)
return result
except HTTPException:
raise
except Exception as e:
logger.error("Model status update failed",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to update model status"
)
@router.post("/tenants/{tenant_id}/members", response_model=TenantMemberResponse)
async def add_team_member(
@track_endpoint_metrics("tenant_add_member")
async def add_team_member_enhanced(
user_id: str,
role: str,
tenant_id: UUID = Path(..., description="Tenant ID"),
current_user: Dict[str, Any] = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Add a team member to tenant with enhanced validation and role management"""
try:
result = await TenantService.add_team_member(
tenant_id, user_id, role, current_user["user_id"], db
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(f"Add team member failed: {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.delete("/tenants/{tenant_id}")
async def delete_tenant_complete(
tenant_id: str,
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_admin_role),
db: AsyncSession = Depends(get_db)
@router.get("/tenants/{tenant_id}/members", response_model=List[TenantMemberResponse])
@track_endpoint_metrics("tenant_get_members")
async def get_team_members_enhanced(
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)
):
"""
Delete a tenant completely with all associated data.
**WARNING: This operation is irreversible!**
This endpoint:
1. Validates tenant exists and user has permissions
2. Deletes all tenant memberships
3. Deletes tenant subscription data
4. Deletes the tenant record
5. Publishes deletion event
Used by admin user deletion process when a tenant has no other admins.
"""
"""Get all team members for a tenant with enhanced filtering"""
try:
tenant_uuid = uuid.UUID(tenant_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid tenant ID format"
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("/tenants/{tenant_id}/members/{member_user_id}/role", response_model=TenantMemberResponse)
@track_endpoint_metrics("tenant_update_member_role")
async def update_member_role_enhanced(
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:
from app.models.tenants import Tenant, TenantMember, Subscription
result = await tenant_service.update_member_role(
str(tenant_id),
member_user_id,
new_role,
current_user["user_id"]
)
return result
# Step 1: Verify tenant exists
tenant_query = select(Tenant).where(Tenant.id == tenant_uuid)
tenant_result = await db.execute(tenant_query)
tenant = tenant_result.scalar_one_or_none()
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("/tenants/{tenant_id}/members/{member_user_id}")
@track_endpoint_metrics("tenant_remove_member")
async def remove_team_member_enhanced(
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 not tenant:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tenant {tenant_id} not found"
)
deletion_stats = {
"tenant_id": tenant_id,
"tenant_name": tenant.name,
"deleted_at": datetime.utcnow().isoformat(),
"memberships_deleted": 0,
"subscriptions_deleted": 0,
"errors": []
}
# Step 2: Delete all tenant memberships
try:
membership_count_query = select(func.count(TenantMember.id)).where(
TenantMember.tenant_id == tenant_uuid
)
membership_count_result = await db.execute(membership_count_query)
membership_count = membership_count_result.scalar()
membership_delete_query = delete(TenantMember).where(
TenantMember.tenant_id == tenant_uuid
)
await db.execute(membership_delete_query)
deletion_stats["memberships_deleted"] = membership_count
logger.info("Deleted tenant memberships",
tenant_id=tenant_id,
count=membership_count)
except Exception as e:
error_msg = f"Error deleting memberships: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
# Step 3: Delete subscription data
try:
subscription_count_query = select(func.count(Subscription.id)).where(
Subscription.tenant_id == tenant_uuid
)
subscription_count_result = await db.execute(subscription_count_query)
subscription_count = subscription_count_result.scalar()
subscription_delete_query = delete(Subscription).where(
Subscription.tenant_id == tenant_uuid
)
await db.execute(subscription_delete_query)
deletion_stats["subscriptions_deleted"] = subscription_count
logger.info("Deleted tenant subscriptions",
tenant_id=tenant_id,
count=subscription_count)
except Exception as e:
error_msg = f"Error deleting subscriptions: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
# Step 4: Delete the tenant record
try:
tenant_delete_query = delete(Tenant).where(Tenant.id == tenant_uuid)
tenant_result = await db.execute(tenant_delete_query)
if tenant_result.rowcount == 0:
raise Exception("Tenant record was not deleted")
await db.commit()
logger.info("Tenant deleted successfully",
tenant_id=tenant_id,
tenant_name=tenant.name)
except Exception as e:
await db.rollback()
error_msg = f"Error deleting tenant record: {str(e)}"
deletion_stats["errors"].append(error_msg)
logger.error(error_msg)
if success:
return {"success": True, "message": "Team member removed successfully"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_msg
detail="Failed to remove team member"
)
# Step 5: Publish tenant deletion event
try:
await publish_tenant_deleted_event(tenant_id, deletion_stats)
except Exception as e:
logger.warning("Failed to publish tenant deletion event", error=str(e))
return {
"success": True,
"message": f"Tenant {tenant_id} deleted successfully",
"deletion_details": deletion_stats
}
except HTTPException:
raise
except Exception as e:
logger.error("Unexpected error deleting tenant",
tenant_id=tenant_id,
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=f"Failed to delete tenant: {str(e)}"
detail="Failed to remove team member"
)
@router.get("/tenants/user/{user_id}")
async def get_user_tenants(
user_id: str,
current_user = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@router.post("/tenants/{tenant_id}/deactivate")
@track_endpoint_metrics("tenant_deactivate")
async def deactivate_tenant_enhanced(
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 tenant memberships for a user (admin only)"""
# Check if this is a service call or admin user
user_type = current_user.get('type', '')
user_role = current_user.get('role', '').lower()
service_name = current_user.get('service', '')
logger.info("The user_type and user_role", user_type=user_type, user_role=user_role)
# ✅ IMPROVED: Accept service tokens OR admin users
is_service_token = (user_type == 'service' or service_name in ['auth', 'admin'])
is_admin_user = (user_role == 'admin')
if not (is_service_token or is_admin_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin role or service authentication required"
)
"""Deactivate a tenant (owner only) with enhanced validation"""
try:
user_uuid = uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
try:
from app.models.tenants import TenantMember, Tenant
# Get all memberships for the user
membership_query = select(TenantMember, Tenant).join(
Tenant, TenantMember.tenant_id == Tenant.id
).where(TenantMember.user_id == user_uuid)
result = await db.execute(membership_query)
memberships_data = result.all()
memberships = []
for membership, tenant in memberships_data:
memberships.append({
"user_id": str(membership.user_id),
"tenant_id": str(membership.tenant_id),
"tenant_name": tenant.name,
"role": membership.role,
"joined_at": membership.created_at.isoformat() if membership.created_at else None
})
return {
"user_id": user_id,
"total_tenants": len(memberships),
"memberships": memberships
}
except Exception as e:
logger.error("Failed to get user tenants", user_id=user_id, error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get user tenants"
)
@router.get("/tenants/{tenant_id}/check-other-admins/{user_id}")
async def check_tenant_has_other_admins(
tenant_id: str,
user_id: str,
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_admin_role),
db: AsyncSession = Depends(get_db)
):
"""Check if tenant has other admin users besides the specified user"""
try:
tenant_uuid = uuid.UUID(tenant_id)
user_uuid = uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid UUID format"
)
try:
from app.models.tenants import TenantMember
# Count admin/owner members excluding the specified user
admin_count_query = select(func.count(TenantMember.id)).where(
TenantMember.tenant_id == tenant_uuid,
TenantMember.role.in_(['admin', 'owner']),
TenantMember.user_id != user_uuid
success = await tenant_service.deactivate_tenant(
str(tenant_id),
current_user["user_id"]
)
result = await db.execute(admin_count_query)
admin_count = result.scalar()
return {
"tenant_id": tenant_id,
"excluded_user_id": user_id,
"has_other_admins": admin_count > 0,
"other_admin_count": admin_count
}
except Exception as e:
logger.error("Failed to check tenant admins",
tenant_id=tenant_id,
user_id=user_id,
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to check tenant admins"
)
@router.post("/tenants/{tenant_id}/transfer-ownership")
async def transfer_tenant_ownership(
tenant_id: str,
transfer_data: dict, # {"current_owner_id": str, "new_owner_id": str}
current_user = Depends(get_current_user_dep),
_admin_check = Depends(require_admin_role),
db: AsyncSession = Depends(get_db)
):
"""Transfer tenant ownership from one user to another (admin only)"""
try:
tenant_uuid = uuid.UUID(tenant_id)
current_owner_id = uuid.UUID(transfer_data.get("current_owner_id"))
new_owner_id = uuid.UUID(transfer_data.get("new_owner_id"))
except (ValueError, TypeError):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid UUID format in request data"
)
try:
from app.models.tenants import TenantMember, Tenant
# Verify tenant exists
tenant_query = select(Tenant).where(Tenant.id == tenant_uuid)
tenant_result = await db.execute(tenant_query)
tenant = tenant_result.scalar_one_or_none()
if not tenant:
if success:
return {"success": True, "message": "Tenant deactivated successfully"}
else:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tenant {tenant_id} not found"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to deactivate tenant"
)
# Get current owner membership
current_owner_query = select(TenantMember).where(
TenantMember.tenant_id == tenant_uuid,
TenantMember.user_id == current_owner_id,
TenantMember.role == 'owner'
)
current_owner_result = await db.execute(current_owner_query)
current_owner_membership = current_owner_result.scalar_one_or_none()
if not current_owner_membership:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Current owner membership not found"
)
# Get new owner membership (should be admin)
new_owner_query = select(TenantMember).where(
TenantMember.tenant_id == tenant_uuid,
TenantMember.user_id == new_owner_id
)
new_owner_result = await db.execute(new_owner_query)
new_owner_membership = new_owner_result.scalar_one_or_none()
if not new_owner_membership:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="New owner must be a member of the tenant"
)
if new_owner_membership.role not in ['admin', 'owner']:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New owner must have admin or owner role"
)
# Perform the transfer
current_owner_membership.role = 'admin' # Demote current owner to admin
new_owner_membership.role = 'owner' # Promote new owner
current_owner_membership.updated_at = datetime.utcnow()
new_owner_membership.updated_at = datetime.utcnow()
await db.commit()
logger.info("Tenant ownership transferred",
tenant_id=tenant_id,
from_user=str(current_owner_id),
to_user=str(new_owner_id))
return {
"success": True,
"message": "Ownership transferred successfully",
"tenant_id": tenant_id,
"previous_owner": str(current_owner_id),
"new_owner": str(new_owner_id),
"transferred_at": datetime.utcnow().isoformat()
}
except HTTPException:
raise
except Exception as e:
await db.rollback()
logger.error("Failed to transfer tenant ownership",
tenant_id=tenant_id,
logger.error("Tenant deactivation failed",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to transfer tenant ownership"
detail="Failed to deactivate tenant"
)
@router.delete("/tenants/user/{user_id}/memberships")
async def delete_user_memberships(
user_id: str,
current_user = Depends(get_current_user_dep),
db: AsyncSession = Depends(get_db)
@router.post("/tenants/{tenant_id}/activate")
@track_endpoint_metrics("tenant_activate")
async def activate_tenant_enhanced(
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)
):
# Check if this is a service call or admin user
user_type = current_user.get('type', '')
user_role = current_user.get('role', '').lower()
service_name = current_user.get('service', '')
logger.info("The user_type and user_role", user_type=user_type, user_role=user_role)
# ✅ IMPROVED: Accept service tokens OR admin users
is_service_token = (user_type == 'service' or service_name in ['auth', 'admin'])
is_admin_user = (user_role == 'admin')
if not (is_service_token or is_admin_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin role or service authentication required"
)
"""Delete all tenant memberships for a user (admin only)"""
try:
user_uuid = uuid.UUID(user_id)
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid user ID format"
)
"""Activate a previously deactivated tenant (owner only) with enhanced validation"""
try:
from app.models.tenants import TenantMember
# Count memberships before deletion
count_query = select(func.count(TenantMember.id)).where(
TenantMember.user_id == user_uuid
success = await tenant_service.activate_tenant(
str(tenant_id),
current_user["user_id"]
)
count_result = await db.execute(count_query)
membership_count = count_result.scalar()
# Delete all memberships
delete_query = delete(TenantMember).where(TenantMember.user_id == user_uuid)
delete_result = await db.execute(delete_query)
await db.commit()
logger.info("Deleted user memberships",
user_id=user_id,
memberships_deleted=delete_result.rowcount)
return {
"success": True,
"user_id": user_id,
"memberships_deleted": delete_result.rowcount,
"expected_count": membership_count,
"deleted_at": datetime.utcnow().isoformat()
}
if success:
return {"success": True, "message": "Tenant activated successfully"}
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to activate tenant"
)
except HTTPException:
raise
except Exception as e:
await db.rollback()
logger.error("Failed to delete user memberships", user_id=user_id, error=str(e))
logger.error("Tenant activation failed",
tenant_id=str(tenant_id),
error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to delete user memberships"
detail="Failed to activate tenant"
)
@router.get("/tenants/statistics", dependencies=[Depends(require_admin_role_dep)])
@track_endpoint_metrics("tenant_get_statistics")
async def get_tenant_statistics_enhanced(
current_user: Dict[str, Any] = Depends(get_current_user_dep),
tenant_service: EnhancedTenantService = Depends(get_enhanced_tenant_service)
):
"""Get comprehensive tenant statistics (admin only) with enhanced analytics"""
try:
stats = await tenant_service.get_tenant_statistics()
return stats
except Exception as e:
logger.error("Get tenant statistics failed", error=str(e))
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Failed to get tenant statistics"
)