Add user delete process
This commit is contained in:
@@ -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