""" Tenant CRUD-endpoints för LandveX Admin Backend. Inkluderar audit-logging, validering, felhantering och schema-isolering. """ from typing import Optional, Dict, Any from uuid import UUID from fastapi import APIRouter, Depends, Request, HTTPException, status, Query from app.database import get_db from app.dependencies import get_current_user, require_admin from app.schemas import ( TenantCreate, TenantUpdate, TenantOut, TenantDetailOut, TenantListOut, TenantListParams, TenantStatusUpdate, TenantActivateRequest, TenantSuspendRequest, TenantStats, SuccessResponse, AuditLogListOut ) from app.services.tenant_service import TenantService from app.services.audit_service import AuditService from app.models import User router = APIRouter(prefix="/tenants", tags=["Tenants"]) def _request_context(request: Request, current_user: User) -> Dict[str, Any]: """Bygg request context för audit-logging.""" return { "request_id": request.headers.get("X-Request-ID", ""), "request_method": request.method, "request_path": str(request.url.path), "actor_ip": request.client.host if request.client else None, "actor_user_agent": request.headers.get("user-agent", ""), } # ─── LIST ───────────────────────────────────────────────────────────────────── @router.get("", response_model=TenantListOut) async def list_tenants( request: Request, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), status: Optional[str] = Query(None), tenant_type: Optional[str] = Query(None), search: Optional[str] = Query(None, max_length=100), country: Optional[str] = Query(None, max_length=2), city: Optional[str] = Query(None, max_length=100), sort_by: str = Query("created_at"), sort_order: str = Query("desc", pattern=r"^(asc|desc)$"), is_active: Optional[bool] = Query(None), current_user: User = Depends(require_admin), db=Depends(get_db), ): """Lista alla tenants med paginering, filtrering och sortering.""" from app.models import TenantStatus as TS, TenantType as TT params = TenantListParams( page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order, search=search, country=country, city=city, is_active=is_active ) if status: try: params.status = TS(status) except ValueError: raise HTTPException(status_code=400, detail=f"Invalid status: {status}") if tenant_type: try: params.tenant_type = TT(tenant_type) except ValueError: raise HTTPException(status_code=400, detail=f"Invalid tenant_type: {tenant_type}") service = TenantService(db) tenants, total = await service.list_tenants(params) pages = (total + page_size - 1) // page_size return TenantListOut( items=[TenantOut.model_validate(t.to_dict()) for t in tenants], total=total, page=page, page_size=page_size, pages=pages ) # ─── STATS ──────────────────────────────────────────────────────────────────── @router.get("/stats", response_model=TenantStats) async def get_tenant_stats( current_user: User = Depends(require_admin), db=Depends(get_db), ): """Hämta aggregerad statistik om alla tenants.""" service = TenantService(db) stats = await service.get_statistics() return TenantStats.model_validate(stats) # ─── GET ONE ────────────────────────────────────────────────────────────────── @router.get("/{tenant_id}", response_model=TenantDetailOut) async def get_tenant( tenant_id: UUID, current_user: User = Depends(get_current_user), db=Depends(get_db), ): """Hämta en tenant med ID. Admin ser allt, andra bara sin egen.""" service = TenantService(db) tenant = await service.get_by_id(tenant_id) if tenant is None: raise HTTPException(status_code=404, detail="Tenant not found") user_role = current_user.role.value if hasattr(current_user.role, 'value') else str(current_user.role) user_tenant = str(current_user.tenant_id) if current_user.tenant_id else None if user_role != "admin" and user_tenant != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied for this tenant") include_sensitive = user_role == "admin" return tenant.to_dict(include_sensitive=include_sensitive) # ─── CREATE ─────────────────────────────────────────────────────────────────── @router.post("", response_model=TenantDetailOut, status_code=status.HTTP_201_CREATED) async def create_tenant( request: Request, data: TenantCreate, current_user: User = Depends(require_admin), db=Depends(get_db), ): """Skapa en ny tenant (kommun eller företag).""" service = TenantService(db) tenant = await service.create_tenant( data=data, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) return tenant.to_dict(include_sensitive=True) # ─── UPDATE ─────────────────────────────────────────────────────────────────── @router.patch("/{tenant_id}", response_model=TenantDetailOut) async def update_tenant( request: Request, tenant_id: UUID, data: TenantUpdate, current_user: User = Depends(get_current_user), db=Depends(get_db), ): """Uppdatera en tenant. Endast angivna fält uppdateras.""" user_role = current_user.role.value if hasattr(current_user.role, 'value') else str(current_user.role) user_tenant = str(current_user.tenant_id) if current_user.tenant_id else None if user_role != "admin" and user_tenant != str(tenant_id): raise HTTPException(status_code=403, detail="Access denied for this tenant") service = TenantService(db) tenant = await service.update_tenant( tenant_id=tenant_id, data=data, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) include_sensitive = user_role == "admin" return tenant.to_dict(include_sensitive=include_sensitive) # ─── DELETE (soft) ──────────────────────────────────────────────────────────── @router.delete("/{tenant_id}", response_model=SuccessResponse) async def delete_tenant( request: Request, tenant_id: UUID, current_user: User = Depends(require_admin), db=Depends(get_db), ): """Mjuk-radera en tenant. Datat bevaras men markeras som borttaget.""" service = TenantService(db) tenant = await service.delete_tenant( tenant_id=tenant_id, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) return SuccessResponse( message=f"Tenant '{tenant.name}' has been deleted", data={"tenant_id": str(tenant.id), "deleted_at": tenant.deleted_at.isoformat() if tenant.deleted_at else None} ) # ─── HARD DELETE ────────────────────────────────────────────────────────────── @router.delete("/{tenant_id}/hard", response_model=SuccessResponse) async def hard_delete_tenant( request: Request, tenant_id: UUID, current_user: User = Depends(require_admin), db=Depends(get_db), ): """Permanent borttagning av en tenant. Åtgärden kan INTE ångras!""" service = TenantService(db) await service.hard_delete_tenant( tenant_id=tenant_id, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) return SuccessResponse(message=f"Tenant {tenant_id} has been permanently deleted") # ─── ACTIVATE ───────────────────────────────────────────────────────────────── @router.post("/{tenant_id}/activate", response_model=TenantDetailOut) async def activate_tenant( request: Request, tenant_id: UUID, data: TenantActivateRequest = TenantActivateRequest(), current_user: User = Depends(require_admin), db=Depends(get_db), ): """Aktivera en tenant (PENDING/TRIAL → ACTIVE).""" service = TenantService(db) tenant = await service.activate_tenant( tenant_id=tenant_id, data=data, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) return tenant.to_dict(include_sensitive=True) # ─── SUSPEND ────────────────────────────────────────────────────────────────── @router.post("/{tenant_id}/suspend", response_model=TenantDetailOut) async def suspend_tenant( request: Request, tenant_id: UUID, data: TenantSuspendRequest, current_user: User = Depends(require_admin), db=Depends(get_db), ): """Suspendera en tenant tillfälligt. Kan återaktiveras senare.""" service = TenantService(db) tenant = await service.suspend_tenant( tenant_id=tenant_id, data=data, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) return tenant.to_dict(include_sensitive=True) # ─── CANCEL ─────────────────────────────────────────────────────────────────── @router.post("/{tenant_id}/cancel", response_model=TenantDetailOut) async def cancel_tenant( request: Request, tenant_id: UUID, reason: Optional[str] = None, current_user: User = Depends(require_admin), db=Depends(get_db), ): """Avsluta en tenant-prenumeration. Typiskt oåterkallbart.""" service = TenantService(db) tenant = await service.cancel_tenant( tenant_id=tenant_id, reason=reason, actor_id=str(current_user.id), request_context=_request_context(request, current_user) ) return tenant.to_dict(include_sensitive=True) # ─── AUDIT LOGS ─────────────────────────────────────────────────────────────── @router.get("/{tenant_id}/audit-logs", response_model=AuditLogListOut) async def get_tenant_audit_logs( tenant_id: UUID, limit: int = Query(50, ge=1, le=200), offset: int = Query(0, ge=0), current_user: User = Depends(require_admin), db=Depends(get_db), ): """Hämta audit-loggar för en specifik tenant.""" audit_service = AuditService(db) logs, total = await audit_service.get_by_tenant( tenant_id=str(tenant_id), limit=limit, offset=offset ) return AuditLogListOut( items=[log.to_dict() for log in logs], total=total, limit=limit, offset=offset )