aee0f09db8
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
293 lines
12 KiB
Python
293 lines
12 KiB
Python
"""
|
|
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
|
|
)
|