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

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

View File

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

View File

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