Add user delete process

This commit is contained in:
Urtzi Alfaro
2025-10-31 11:54:19 +01:00
parent 63f5c6d512
commit 269d3b5032
74 changed files with 16783 additions and 213 deletions

View File

@@ -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}")

View File

@@ -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