Add user delete process
This commit is contained in:
@@ -13,8 +13,10 @@ from sqlalchemy import select
|
||||
from shared.auth.decorators import get_current_user_dep, require_admin_role_dep
|
||||
from shared.routing import RouteBuilder
|
||||
from app.core.database import get_db
|
||||
from app.models.tenants import Subscription
|
||||
from app.models.tenants import Subscription, Tenant
|
||||
from app.services.subscription_limit_service import SubscriptionLimitService
|
||||
from shared.clients.stripe_client import StripeProvider
|
||||
from app.core.config import settings
|
||||
|
||||
logger = structlog.get_logger()
|
||||
router = APIRouter()
|
||||
@@ -65,6 +67,18 @@ class SubscriptionStatusResponse(BaseModel):
|
||||
days_until_inactive: int | None
|
||||
|
||||
|
||||
class InvoiceResponse(BaseModel):
|
||||
"""Response model for an invoice"""
|
||||
id: str
|
||||
date: str
|
||||
amount: float
|
||||
currency: str
|
||||
status: str
|
||||
description: str | None = None
|
||||
invoice_pdf: str | None = None
|
||||
hosted_invoice_url: str | None = None
|
||||
|
||||
|
||||
@router.post("/api/v1/subscriptions/cancel", response_model=SubscriptionCancellationResponse)
|
||||
async def cancel_subscription(
|
||||
request: SubscriptionCancellationRequest,
|
||||
@@ -251,3 +265,65 @@ async def get_subscription_status(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to get subscription status"
|
||||
)
|
||||
|
||||
|
||||
@router.get("/api/v1/subscriptions/{tenant_id}/invoices", response_model=list[InvoiceResponse])
|
||||
async def get_tenant_invoices(
|
||||
tenant_id: str,
|
||||
current_user: dict = Depends(get_current_user_dep),
|
||||
db: AsyncSession = Depends(get_db)
|
||||
):
|
||||
"""
|
||||
Get invoice history for a tenant from Stripe
|
||||
"""
|
||||
try:
|
||||
# Verify tenant exists
|
||||
query = select(Tenant).where(Tenant.id == UUID(tenant_id))
|
||||
result = await db.execute(query)
|
||||
tenant = result.scalar_one_or_none()
|
||||
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Check if tenant has a Stripe customer ID
|
||||
if not tenant.stripe_customer_id:
|
||||
logger.info("no_stripe_customer_id", tenant_id=tenant_id)
|
||||
return []
|
||||
|
||||
# Initialize Stripe provider
|
||||
stripe_provider = StripeProvider(
|
||||
api_key=settings.STRIPE_SECRET_KEY,
|
||||
webhook_secret=settings.STRIPE_WEBHOOK_SECRET
|
||||
)
|
||||
|
||||
# Fetch invoices from Stripe
|
||||
stripe_invoices = await stripe_provider.get_invoices(tenant.stripe_customer_id)
|
||||
|
||||
# Transform to response format
|
||||
invoices = []
|
||||
for invoice in stripe_invoices:
|
||||
invoices.append(InvoiceResponse(
|
||||
id=invoice.id,
|
||||
date=invoice.created_at.strftime('%Y-%m-%d'),
|
||||
amount=invoice.amount,
|
||||
currency=invoice.currency.upper(),
|
||||
status=invoice.status,
|
||||
description=invoice.description,
|
||||
invoice_pdf=invoice.invoice_pdf,
|
||||
hosted_invoice_url=invoice.hosted_invoice_url
|
||||
))
|
||||
|
||||
logger.info("invoices_retrieved", tenant_id=tenant_id, count=len(invoices))
|
||||
return invoices
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("get_invoices_failed", error=str(e), tenant_id=tenant_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Failed to retrieve invoices"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
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
|
||||
@@ -269,3 +269,157 @@ async def remove_team_member(
|
||||
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"
|
||||
)
|
||||
|
||||
@@ -98,3 +98,56 @@ async def update_tenant(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Tenant update failed"
|
||||
)
|
||||
|
||||
@router.delete(route_builder.build_base_route("{tenant_id}", include_tenant_prefix=False))
|
||||
@track_endpoint_metrics("tenant_delete")
|
||||
async def delete_tenant(
|
||||
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)
|
||||
):
|
||||
"""Delete tenant and all associated data - ATOMIC operation (Owner/Admin or System only)"""
|
||||
|
||||
logger.info(
|
||||
"Tenant DELETE request received",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
user_type=current_user.get("type", "user"),
|
||||
is_service=current_user.get("type") == "service",
|
||||
role=current_user.get("role"),
|
||||
service_name=current_user.get("service", "none")
|
||||
)
|
||||
|
||||
try:
|
||||
# Allow internal service calls to bypass admin check
|
||||
skip_admin_check = current_user.get("type") == "service"
|
||||
|
||||
result = await tenant_service.delete_tenant(
|
||||
str(tenant_id),
|
||||
requesting_user_id=current_user.get("user_id"),
|
||||
skip_admin_check=skip_admin_check
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Tenant DELETE request successful",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
deleted_items=result.get("deleted_items")
|
||||
)
|
||||
|
||||
return {
|
||||
"message": "Tenant deleted successfully",
|
||||
"summary": result
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Tenant deletion failed",
|
||||
tenant_id=str(tenant_id),
|
||||
user_id=current_user.get("user_id"),
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Tenant deletion failed"
|
||||
)
|
||||
|
||||
@@ -57,4 +57,18 @@ async def publish_tenant_deleted_event(tenant_id: str, deletion_stats: Dict[str,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Failed to publish tenant deletion event", error=str(e))
|
||||
logger.error("Failed to publish tenant deletion event", error=str(e))
|
||||
|
||||
async def publish_tenant_deleted(tenant_id: str, tenant_name: str):
|
||||
"""Publish tenant deleted event (simple version)"""
|
||||
try:
|
||||
await data_publisher.publish_event(
|
||||
"tenant.deleted",
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"tenant_name": tenant_name,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to publish tenant.deleted event: {e}")
|
||||
@@ -698,7 +698,7 @@ class EnhancedTenantService:
|
||||
session: AsyncSession = None
|
||||
) -> bool:
|
||||
"""Activate a previously deactivated tenant (admin only)"""
|
||||
|
||||
|
||||
try:
|
||||
# Verify user is owner
|
||||
access = await self.verify_user_access(user_id, tenant_id)
|
||||
@@ -707,26 +707,26 @@ class EnhancedTenantService:
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only tenant owner can activate tenant"
|
||||
)
|
||||
|
||||
|
||||
activated_tenant = await self.tenant_repo.activate_tenant(tenant_id)
|
||||
|
||||
|
||||
if not activated_tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
|
||||
# Also reactivate subscription if exists
|
||||
subscription = await self.subscription_repo.get_subscription_by_tenant(tenant_id)
|
||||
if subscription and subscription.status == "suspended":
|
||||
await self.subscription_repo.reactivate_subscription(str(subscription.id))
|
||||
|
||||
|
||||
logger.info("Tenant activated",
|
||||
tenant_id=tenant_id,
|
||||
activated_by=user_id)
|
||||
|
||||
|
||||
return True
|
||||
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
@@ -738,6 +738,342 @@ class EnhancedTenantService:
|
||||
detail="Failed to activate tenant"
|
||||
)
|
||||
|
||||
async def delete_tenant(
|
||||
self,
|
||||
tenant_id: str,
|
||||
requesting_user_id: str = None,
|
||||
skip_admin_check: bool = False
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Permanently delete a tenant and all its associated data
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant to delete
|
||||
requesting_user_id: The user requesting deletion (for permission check)
|
||||
skip_admin_check: Skip the admin check (for internal service calls)
|
||||
|
||||
Returns:
|
||||
Dict with deletion summary
|
||||
"""
|
||||
|
||||
try:
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
await self._init_repositories(db_session)
|
||||
|
||||
# Get tenant first to verify it exists
|
||||
tenant = await self.tenant_repo.get_by_id(tenant_id)
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Permission check (unless internal service call)
|
||||
if not skip_admin_check:
|
||||
if not requesting_user_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="User ID required for deletion authorization"
|
||||
)
|
||||
|
||||
access = await self.verify_user_access(requesting_user_id, tenant_id)
|
||||
if not access.has_access or access.role not in ["owner", "admin"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only tenant owner or admin can delete tenant"
|
||||
)
|
||||
|
||||
# Check if there are other admins (protection against accidental deletion)
|
||||
admin_members = await self.member_repo.get_tenant_members(
|
||||
tenant_id,
|
||||
active_only=True,
|
||||
role=None # Get all roles, we'll filter
|
||||
)
|
||||
|
||||
admin_count = sum(1 for m in admin_members if m.role in ["owner", "admin"])
|
||||
|
||||
# Build deletion summary
|
||||
deletion_summary = {
|
||||
"tenant_id": tenant_id,
|
||||
"tenant_name": tenant.name,
|
||||
"admin_count": admin_count,
|
||||
"total_members": len(admin_members),
|
||||
"deleted_items": {},
|
||||
"errors": []
|
||||
}
|
||||
|
||||
# Cancel active subscriptions first
|
||||
try:
|
||||
subscription = await self.subscription_repo.get_active_subscription(tenant_id)
|
||||
if subscription:
|
||||
await self.subscription_repo.cancel_subscription(
|
||||
str(subscription.id),
|
||||
reason="Tenant deleted"
|
||||
)
|
||||
deletion_summary["deleted_items"]["subscriptions"] = 1
|
||||
except Exception as e:
|
||||
logger.warning("Failed to cancel subscription during tenant deletion",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
deletion_summary["errors"].append(f"Subscription cancellation: {str(e)}")
|
||||
|
||||
# Delete all tenant memberships (CASCADE will handle this, but we do it explicitly)
|
||||
try:
|
||||
deleted_members = 0
|
||||
for member in admin_members:
|
||||
try:
|
||||
await self.member_repo.delete(str(member.id))
|
||||
deleted_members += 1
|
||||
except Exception as e:
|
||||
logger.warning("Failed to delete membership",
|
||||
membership_id=member.id,
|
||||
error=str(e))
|
||||
|
||||
deletion_summary["deleted_items"]["memberships"] = deleted_members
|
||||
except Exception as e:
|
||||
logger.warning("Failed to delete memberships during tenant deletion",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
deletion_summary["errors"].append(f"Membership deletion: {str(e)}")
|
||||
|
||||
# Finally, delete the tenant itself (CASCADE should handle related records)
|
||||
try:
|
||||
await self.tenant_repo.delete(tenant_id)
|
||||
deletion_summary["deleted_items"]["tenant"] = 1
|
||||
except Exception as e:
|
||||
logger.error("Failed to delete tenant record",
|
||||
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)}"
|
||||
)
|
||||
|
||||
# Publish deletion event for other services
|
||||
try:
|
||||
from app.services.messaging import publish_tenant_deleted
|
||||
await publish_tenant_deleted(tenant_id, tenant.name)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to publish tenant deletion event",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
deletion_summary["errors"].append(f"Event publishing: {str(e)}")
|
||||
|
||||
logger.info("Tenant deleted successfully",
|
||||
tenant_id=tenant_id,
|
||||
tenant_name=tenant.name,
|
||||
deleted_by=requesting_user_id,
|
||||
summary=deletion_summary)
|
||||
|
||||
return deletion_summary
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("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)}"
|
||||
)
|
||||
|
||||
async def delete_user_memberships(
|
||||
self,
|
||||
user_id: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete all tenant memberships for a user
|
||||
Used when deleting a user from the auth service
|
||||
|
||||
Args:
|
||||
user_id: The user whose memberships should be deleted
|
||||
|
||||
Returns:
|
||||
Dict with deletion summary
|
||||
"""
|
||||
|
||||
try:
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
await self._init_repositories(db_session)
|
||||
|
||||
# Get all user memberships
|
||||
memberships = await self.member_repo.get_user_memberships(user_id, active_only=False)
|
||||
|
||||
deleted_count = 0
|
||||
errors = []
|
||||
|
||||
for membership in memberships:
|
||||
try:
|
||||
# Delete the membership
|
||||
await self.member_repo.delete(str(membership.id))
|
||||
deleted_count += 1
|
||||
except Exception as e:
|
||||
logger.warning("Failed to delete membership",
|
||||
membership_id=membership.id,
|
||||
user_id=user_id,
|
||||
tenant_id=membership.tenant_id,
|
||||
error=str(e))
|
||||
errors.append(f"Membership {membership.id}: {str(e)}")
|
||||
|
||||
logger.info("User memberships deleted",
|
||||
user_id=user_id,
|
||||
total_memberships=len(memberships),
|
||||
deleted_count=deleted_count,
|
||||
errors=len(errors))
|
||||
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"total_memberships": len(memberships),
|
||||
"deleted_count": deleted_count,
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error deleting user memberships",
|
||||
user_id=user_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to delete user memberships: {str(e)}"
|
||||
)
|
||||
|
||||
async def transfer_tenant_ownership(
|
||||
self,
|
||||
tenant_id: str,
|
||||
current_owner_id: str,
|
||||
new_owner_id: str,
|
||||
requesting_user_id: str = None
|
||||
) -> TenantResponse:
|
||||
"""
|
||||
Transfer tenant ownership to another admin
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant whose ownership to transfer
|
||||
current_owner_id: Current owner (for verification)
|
||||
new_owner_id: New owner (must be an existing admin)
|
||||
requesting_user_id: User requesting the transfer (for permission check)
|
||||
|
||||
Returns:
|
||||
Updated tenant
|
||||
"""
|
||||
|
||||
try:
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
async with UnitOfWork(db_session) as uow:
|
||||
# Register repositories
|
||||
tenant_repo = uow.register_repository("tenants", TenantRepository, Tenant)
|
||||
member_repo = uow.register_repository("members", TenantMemberRepository, TenantMember)
|
||||
|
||||
# Get tenant
|
||||
tenant = await tenant_repo.get_by_id(tenant_id)
|
||||
if not tenant:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Tenant not found"
|
||||
)
|
||||
|
||||
# Verify current ownership
|
||||
if str(tenant.owner_id) != current_owner_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Current owner ID does not match"
|
||||
)
|
||||
|
||||
# Permission check (must be current owner or system)
|
||||
if requesting_user_id and requesting_user_id != current_owner_id:
|
||||
access = await self.verify_user_access(requesting_user_id, tenant_id)
|
||||
if not access.has_access or access.role != "owner":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Only current owner can transfer ownership"
|
||||
)
|
||||
|
||||
# Verify new owner is an admin
|
||||
new_owner_membership = await member_repo.get_membership(tenant_id, new_owner_id)
|
||||
if not new_owner_membership or not new_owner_membership.is_active:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="New owner must be an active 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 be an admin"
|
||||
)
|
||||
|
||||
# Update tenant owner
|
||||
updated_tenant = await tenant_repo.update(tenant_id, {
|
||||
"owner_id": new_owner_id
|
||||
})
|
||||
|
||||
# Update memberships: current owner -> admin, new owner -> owner
|
||||
current_owner_membership = await member_repo.get_membership(tenant_id, current_owner_id)
|
||||
if current_owner_membership:
|
||||
await member_repo.update_member_role(tenant_id, current_owner_id, "admin")
|
||||
|
||||
await member_repo.update_member_role(tenant_id, new_owner_id, "owner")
|
||||
|
||||
# Commit transaction
|
||||
await uow.commit()
|
||||
|
||||
logger.info("Tenant ownership transferred",
|
||||
tenant_id=tenant_id,
|
||||
from_owner=current_owner_id,
|
||||
to_owner=new_owner_id,
|
||||
requested_by=requesting_user_id)
|
||||
|
||||
return TenantResponse.from_orm(updated_tenant)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error("Error transferring tenant ownership",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to transfer ownership: {str(e)}"
|
||||
)
|
||||
|
||||
async def get_tenant_admins(
|
||||
self,
|
||||
tenant_id: str
|
||||
) -> List[TenantMemberResponse]:
|
||||
"""
|
||||
Get all admins (owner + admins) for a tenant
|
||||
Used by auth service to check for other admins before tenant deletion
|
||||
|
||||
Args:
|
||||
tenant_id: The tenant to query
|
||||
|
||||
Returns:
|
||||
List of admin members
|
||||
"""
|
||||
|
||||
try:
|
||||
async with self.database_manager.get_session() as db_session:
|
||||
await self._init_repositories(db_session)
|
||||
|
||||
# Get all active members
|
||||
all_members = await self.member_repo.get_tenant_members(
|
||||
tenant_id,
|
||||
active_only=True,
|
||||
include_user_info=True
|
||||
)
|
||||
|
||||
# Filter to just admins and owner
|
||||
admin_members = [m for m in all_members if m.role in ["owner", "admin"]]
|
||||
|
||||
return [TenantMemberResponse.from_orm(m) for m in admin_members]
|
||||
|
||||
except Exception as e:
|
||||
logger.error("Error getting tenant admins",
|
||||
tenant_id=tenant_id,
|
||||
error=str(e))
|
||||
return []
|
||||
|
||||
|
||||
# Legacy compatibility alias
|
||||
TenantService = EnhancedTenantService
|
||||
|
||||
Reference in New Issue
Block a user