# services/tenant/app/api/tenants.py """ Tenant API endpoints """ 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 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 ) from app.services.tenant_service import TenantService from shared.auth.decorators import ( get_current_user_dep, require_admin_role ) logger = structlog.get_logger() router = APIRouter() @router.post("/tenants/register", response_model=TenantResponse) async def register_bakery( bakery_data: BakeryRegistration, current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): 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']}") return result except Exception as e: logger.error(f"Bakery registration failed: {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, tenant_id: UUID = Path(..., description="Tenant ID"), db: AsyncSession = Depends(get_db) ): """Verify if user has access to tenant - Called by Gateway""" # Check if this is a service request if user_id in ["training-service", "data-service", "forecasting-service"]: # Services have access to all tenants for their operations return TenantAccessResponse( has_access=True, role="service", permissions=["read", "write"] ) try: access_info = await TenantService.verify_user_access(user_id, tenant_id, db) return access_info except Exception as e: logger.error(f"Access verification failed: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Access verification failed" ) @router.get("/tenants/users/{user_id}", response_model=List[TenantResponse]) async def get_user_tenants( user_id: str, current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): # Users can only see their own tenants if current_user["user_id"] != user_id: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Access denied" ) try: tenants = await TenantService.get_user_tenants(user_id, db) return tenants except Exception as e: logger.error(f"Failed to get user tenants: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to retrieve tenants" ) @router.get("/tenants/{tenant_id}", response_model=TenantResponse) async def get_tenant( tenant_id: UUID = Path(..., description="Tenant ID"), current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): # Verify user has access to tenant access = await TenantService.verify_user_access(current_user["user_id"], tenant_id, db) 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) if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found" ) return tenant @router.put("/tenants/{tenant_id}", response_model=TenantResponse) async def update_tenant( 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) ): try: result = await TenantService.update_tenant(tenant_id, update_data, current_user["user_id"], db) return result except HTTPException: raise except Exception as e: logger.error(f"Tenant update failed: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Tenant update failed" ) @router.post("/tenants/{tenant_id}/members", response_model=TenantMemberResponse) 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), db: AsyncSession = Depends(get_db) ): try: result = await TenantService.add_team_member( tenant_id, user_id, role, current_user["user_id"], db ) return result except HTTPException: raise except Exception as e: logger.error(f"Add team member failed: {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) ): """ 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. """ try: tenant_uuid = uuid.UUID(tenant_id) except ValueError: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid tenant ID format" ) try: from app.models.tenants import Tenant, TenantMember, Subscription # 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() 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) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=error_msg ) # 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, error=str(e)) raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete tenant: {str(e)}" )