landvex: Fixar och tester klara för alla komponenter
- Datafabrik: Dockerfile fix, agentorkestrering fungerar - Vision: Identify-modell, FAISS, OCR alla testade - API: Alla 7 integrationstester passerade - Upplösare: Entitetsupplösning verifierad
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
"""
|
||||
LandveX Admin Backend — Audit Log API Routes
|
||||
Endpoints for querying audit logs
|
||||
"""
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, HTTPException, status
|
||||
|
||||
from app.core.security import get_current_user, require_admin
|
||||
from app.api.dependencies import get_audit_service, get_request_context
|
||||
from app.services.audit_service import AuditService
|
||||
from app.schemas.common import AuditLogResponse
|
||||
|
||||
|
||||
router = APIRouter(prefix="/audit-logs", tags=["audit"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
summary="List audit logs",
|
||||
description="Query audit logs with filtering"
|
||||
)
|
||||
async def list_audit_logs(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
entity_type: Optional[str] = Query(None, description="Filter by entity type"),
|
||||
entity_id: Optional[str] = Query(None, description="Filter by entity ID"),
|
||||
actor_id: Optional[str] = Query(None, description="Filter by actor"),
|
||||
action: Optional[str] = Query(None, description="Filter by action type"),
|
||||
audit_service: AuditService = Depends(get_audit_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin)
|
||||
):
|
||||
"""List audit logs with filtering.
|
||||
|
||||
Admin access required.
|
||||
"""
|
||||
logs = await audit_service.get_recent(limit=limit)
|
||||
|
||||
# Apply filters in memory (for small datasets)
|
||||
# In production, add database-level filtering
|
||||
if entity_type:
|
||||
logs = [l for l in logs if l.entity_type == entity_type]
|
||||
if entity_id:
|
||||
logs = [l for l in logs if l.entity_id == entity_id]
|
||||
if actor_id:
|
||||
logs = [l for l in logs if l.actor_id == actor_id]
|
||||
if action:
|
||||
from app.models.audit_log import AuditAction
|
||||
try:
|
||||
action_enum = AuditAction(action)
|
||||
logs = [l for l in logs if l.action == action_enum]
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return {
|
||||
"items": [log.to_dict() for log in logs],
|
||||
"total": len(logs),
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/entity/{entity_type}/{entity_id}",
|
||||
summary="Get entity audit history",
|
||||
description="Get all audit logs for a specific entity"
|
||||
)
|
||||
async def get_entity_audit_logs(
|
||||
entity_type: str,
|
||||
entity_id: str,
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
audit_service: AuditService = Depends(get_audit_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin)
|
||||
):
|
||||
"""Get audit logs for a specific entity."""
|
||||
logs, total = await audit_service.get_by_entity(
|
||||
entity_type=entity_type,
|
||||
entity_id=entity_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [log.to_dict() for log in logs],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/actor/{actor_id}",
|
||||
summary="Get actor audit history",
|
||||
description="Get all audit logs for a specific actor (user)"
|
||||
)
|
||||
async def get_actor_audit_logs(
|
||||
actor_id: str,
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
audit_service: AuditService = Depends(get_audit_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin)
|
||||
):
|
||||
"""Get audit logs for a specific actor."""
|
||||
logs, total = await audit_service.get_by_actor(
|
||||
actor_id=actor_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [log.to_dict() for log in logs],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
LandveX Admin Backend — API Dependencies
|
||||
FastAPI dependencies for auth, database, and request context
|
||||
"""
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Depends, Request, HTTPException, status
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.security import get_current_user, require_admin
|
||||
from app.services.tenant_service import TenantService
|
||||
from app.services.audit_service import AuditService
|
||||
from app.core.exceptions import TenantNotFoundException, UnauthorizedTenantAccessException
|
||||
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
async def get_tenant_service(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> TenantService:
|
||||
"""Dependency to get tenant service."""
|
||||
return TenantService(db)
|
||||
|
||||
|
||||
async def get_audit_service(
|
||||
db: AsyncSession = Depends(get_db)
|
||||
) -> AuditService:
|
||||
"""Dependency to get audit service."""
|
||||
return AuditService(db)
|
||||
|
||||
|
||||
async def get_request_context(
|
||||
request: Request,
|
||||
current_user: Optional[Dict[str, Any]] = Depends(get_current_user)
|
||||
) -> Dict[str, Any]:
|
||||
"""Build request context for audit logging."""
|
||||
|
||||
context = {
|
||||
"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", ""),
|
||||
}
|
||||
|
||||
if current_user:
|
||||
context["actor_id"] = current_user.get("id")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
async def get_current_tenant_id(
|
||||
request: Request
|
||||
) -> Optional[str]:
|
||||
"""Extract tenant ID from request (header or path)."""
|
||||
# Try header first
|
||||
tenant_id = request.headers.get("X-Tenant-ID")
|
||||
if tenant_id:
|
||||
return tenant_id
|
||||
|
||||
# Try path parameter
|
||||
tenant_id = request.path_params.get("tenant_id")
|
||||
if tenant_id:
|
||||
return tenant_id
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def require_tenant_admin(
|
||||
tenant_id: UUID,
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
tenant_service: TenantService = Depends(get_tenant_service)
|
||||
) -> Dict[str, Any]:
|
||||
"""Require admin access to a specific tenant."""
|
||||
|
||||
user_role = current_user.get("role", "")
|
||||
user_tenant = current_user.get("tenant_id")
|
||||
|
||||
# Global admin can access all tenants
|
||||
if user_role == "admin":
|
||||
return current_user
|
||||
|
||||
# Tenant admin can only access their own tenant
|
||||
if user_role == "tenant_admin" and str(user_tenant) == str(tenant_id):
|
||||
return current_user
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail=f"Admin access required for tenant {tenant_id}"
|
||||
)
|
||||
|
||||
|
||||
async def optional_auth(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security)
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""Optional authentication - returns user if token valid, None otherwise."""
|
||||
if credentials is None:
|
||||
return None
|
||||
|
||||
from app.core.security import decode_token
|
||||
payload = decode_token(credentials.credentials)
|
||||
if payload is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"id": payload.get("sub"),
|
||||
"email": payload.get("email"),
|
||||
"role": payload.get("role", "user"),
|
||||
"tenant_id": payload.get("tenant_id"),
|
||||
"permissions": payload.get("permissions", []),
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
"""
|
||||
LandveX Admin Backend — Tenant API Routes
|
||||
CRUD endpoints for tenant management with validation and audit logging
|
||||
"""
|
||||
from typing import Optional, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, status, Query
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.db.session import get_db
|
||||
from app.core.security import get_current_user, require_admin
|
||||
from app.core.exceptions import (
|
||||
LandvexException,
|
||||
TenantNotFoundException,
|
||||
TenantAlreadyExistsException,
|
||||
TenantValidationException,
|
||||
)
|
||||
from app.api.dependencies import (
|
||||
get_tenant_service,
|
||||
get_audit_service,
|
||||
get_request_context,
|
||||
require_tenant_admin,
|
||||
)
|
||||
from app.services.tenant_service import TenantService
|
||||
from app.services.audit_service import AuditService
|
||||
from app.schemas.tenant import (
|
||||
TenantCreate, TenantUpdate, TenantResponse, TenantDetailResponse,
|
||||
TenantListResponse, TenantListParams, TenantStatusUpdate,
|
||||
TenantActivateRequest, TenantSuspendRequest, TenantStats,
|
||||
)
|
||||
from app.schemas.common import ErrorResponse, SuccessResponse
|
||||
|
||||
|
||||
router = APIRouter(prefix="/tenants", tags=["tenants"])
|
||||
|
||||
|
||||
# ─── Exception Handlers ───────────────────────────────────────────────────────
|
||||
|
||||
@router.exception_handler(LandvexException)
|
||||
async def landvex_exception_handler(request: Request, exc: LandvexException):
|
||||
"""Handle Landvex exceptions."""
|
||||
return JSONResponse(
|
||||
status_code=exc.status_code,
|
||||
content={
|
||||
"error": exc.message,
|
||||
"details": exc.details,
|
||||
"request_id": request.headers.get("X-Request-ID", ""),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ─── CREATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=TenantDetailResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new tenant",
|
||||
description="Create a new municipality or company tenant with schema isolation"
|
||||
)
|
||||
async def create_tenant(
|
||||
data: TenantCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Create a new tenant.
|
||||
|
||||
- **name**: Display name of the tenant
|
||||
- **slug**: URL-friendly unique identifier
|
||||
- **tenant_type**: Type of tenant (municipality, company, etc.)
|
||||
- **contact_email**: Primary contact email
|
||||
"""
|
||||
try:
|
||||
tenant = await tenant_service.create(
|
||||
data=data,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
except TenantAlreadyExistsException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
except TenantValidationException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
# ─── READ ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=TenantListResponse,
|
||||
summary="List all tenants",
|
||||
description="List tenants with pagination, filtering, and sorting"
|
||||
)
|
||||
async def list_tenants(
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(20, ge=1, le=100, description="Items per page"),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
tenant_type: Optional[str] = Query(None, description="Filter by type"),
|
||||
search: Optional[str] = Query(None, max_length=100, description="Search query"),
|
||||
country: Optional[str] = Query(None, max_length=2, description="Filter by country code"),
|
||||
city: Optional[str] = Query(None, max_length=100, description="Filter by city"),
|
||||
sort_by: str = Query("created_at", description="Sort field"),
|
||||
sort_order: str = Query("desc", pattern=r"^(asc|desc)$", description="Sort direction"),
|
||||
is_active: Optional[bool] = Query(None, description="Filter by active status"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""List all tenants with filtering and pagination."""
|
||||
|
||||
from app.models.tenant import TenantStatus as TS, TenantType as TT
|
||||
|
||||
# Build params
|
||||
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
|
||||
)
|
||||
|
||||
# Parse enum values
|
||||
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}")
|
||||
|
||||
tenants, total = await tenant_service.list(params)
|
||||
|
||||
pages = (total + page_size - 1) // page_size
|
||||
|
||||
return TenantListResponse(
|
||||
items=[TenantResponse.model_validate(t.to_dict()) for t in tenants],
|
||||
total=total,
|
||||
page=page,
|
||||
page_size=page_size,
|
||||
pages=pages
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/stats",
|
||||
response_model=TenantStats,
|
||||
summary="Get tenant statistics",
|
||||
description="Get aggregated statistics about all tenants"
|
||||
)
|
||||
async def get_tenant_stats(
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin)
|
||||
):
|
||||
"""Get tenant statistics."""
|
||||
stats = await tenant_service.get_statistics()
|
||||
return TenantStats.model_validate(stats)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{tenant_id}",
|
||||
response_model=TenantDetailResponse,
|
||||
summary="Get tenant by ID",
|
||||
description="Get detailed information about a specific tenant"
|
||||
)
|
||||
async def get_tenant(
|
||||
tenant_id: UUID,
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Get a tenant by ID."""
|
||||
try:
|
||||
tenant = await tenant_service.get_by_id(tenant_id)
|
||||
|
||||
# Check access (admin can see all, others only their tenant)
|
||||
user_role = current_user.get("role", "")
|
||||
user_tenant = current_user.get("tenant_id")
|
||||
|
||||
if user_role != "admin" and str(user_tenant) != str(tenant_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied for this tenant"
|
||||
)
|
||||
|
||||
# Return sensitive data only for admins
|
||||
include_sensitive = user_role == "admin"
|
||||
return tenant.to_dict(include_sensitive=include_sensitive)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
# ─── UPDATE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.put(
|
||||
"/{tenant_id}",
|
||||
response_model=TenantDetailResponse,
|
||||
summary="Update tenant",
|
||||
description="Update tenant information"
|
||||
)
|
||||
async def update_tenant(
|
||||
tenant_id: UUID,
|
||||
data: TenantUpdate,
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Update a tenant.
|
||||
|
||||
Only provided fields will be updated. Omit fields to keep current values.
|
||||
"""
|
||||
try:
|
||||
# Check permissions
|
||||
user_role = current_user.get("role", "")
|
||||
user_tenant = current_user.get("tenant_id")
|
||||
|
||||
if user_role != "admin" and str(user_tenant) != str(tenant_id):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Access denied for this tenant"
|
||||
)
|
||||
|
||||
tenant = await tenant_service.update(
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
|
||||
include_sensitive = user_role == "admin"
|
||||
return tenant.to_dict(include_sensitive=include_sensitive)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
except TenantValidationException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
# ─── DELETE ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@router.delete(
|
||||
"/{tenant_id}",
|
||||
response_model=SuccessResponse,
|
||||
summary="Delete tenant",
|
||||
description="Soft delete a tenant (can be restored by admin)"
|
||||
)
|
||||
async def delete_tenant(
|
||||
tenant_id: UUID,
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Soft delete a tenant.
|
||||
|
||||
The tenant data is preserved but marked as deleted.
|
||||
Use hard delete for permanent removal (admin only).
|
||||
"""
|
||||
try:
|
||||
tenant = await tenant_service.delete(
|
||||
tenant_id=tenant_id,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
message=f"Tenant '{tenant.name}' has been deleted",
|
||||
data={"tenant_id": str(tenant.id), "deleted_at": tenant.deleted_at.isoformat()}
|
||||
)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
except TenantValidationException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{tenant_id}/hard",
|
||||
response_model=SuccessResponse,
|
||||
summary="Hard delete tenant",
|
||||
description="Permanently delete a tenant and all associated data"
|
||||
)
|
||||
async def hard_delete_tenant(
|
||||
tenant_id: UUID,
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Hard delete a tenant (permanent removal).
|
||||
|
||||
**Warning**: This action cannot be undone. All tenant data will be permanently deleted.
|
||||
"""
|
||||
try:
|
||||
await tenant_service.hard_delete(
|
||||
tenant_id=tenant_id,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
|
||||
return SuccessResponse(
|
||||
message=f"Tenant {tenant_id} has been permanently deleted"
|
||||
)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
# ─── STATUS MANAGEMENT ────────────────────────────────────────────────────────
|
||||
|
||||
@router.post(
|
||||
"/{tenant_id}/activate",
|
||||
response_model=TenantDetailResponse,
|
||||
summary="Activate tenant",
|
||||
description="Activate a pending or trial tenant"
|
||||
)
|
||||
async def activate_tenant(
|
||||
tenant_id: UUID,
|
||||
data: TenantActivateRequest = TenantActivateRequest(),
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Activate a tenant.
|
||||
|
||||
Changes status from PENDING or TRIAL to ACTIVE.
|
||||
"""
|
||||
try:
|
||||
tenant = await tenant_service.activate(
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
except TenantValidationException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{tenant_id}/suspend",
|
||||
response_model=TenantDetailResponse,
|
||||
summary="Suspend tenant",
|
||||
description="Temporarily suspend a tenant"
|
||||
)
|
||||
async def suspend_tenant(
|
||||
tenant_id: UUID,
|
||||
data: TenantSuspendRequest,
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Suspend a tenant.
|
||||
|
||||
Temporarily disables tenant access. Can be reactivated later.
|
||||
"""
|
||||
try:
|
||||
tenant = await tenant_service.suspend(
|
||||
tenant_id=tenant_id,
|
||||
data=data,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
except TenantValidationException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{tenant_id}/cancel",
|
||||
response_model=TenantDetailResponse,
|
||||
summary="Cancel tenant",
|
||||
description="Cancel a tenant subscription"
|
||||
)
|
||||
async def cancel_tenant(
|
||||
tenant_id: UUID,
|
||||
reason: Optional[str] = None,
|
||||
tenant_service: TenantService = Depends(get_tenant_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin),
|
||||
request_context: Dict[str, Any] = Depends(get_request_context)
|
||||
):
|
||||
"""Cancel a tenant subscription.
|
||||
|
||||
Marks tenant as cancelled. This is typically irreversible.
|
||||
"""
|
||||
try:
|
||||
tenant = await tenant_service.cancel(
|
||||
tenant_id=tenant_id,
|
||||
reason=reason,
|
||||
actor_id=current_user.get("id"),
|
||||
request_context=request_context
|
||||
)
|
||||
return tenant.to_dict(include_sensitive=True)
|
||||
|
||||
except TenantNotFoundException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
except TenantValidationException as e:
|
||||
raise HTTPException(status_code=e.status_code, detail=e.message)
|
||||
|
||||
|
||||
# ─── AUDIT LOGS ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get(
|
||||
"/{tenant_id}/audit-logs",
|
||||
summary="Get tenant audit logs",
|
||||
description="Get audit log history for a specific tenant"
|
||||
)
|
||||
async def get_tenant_audit_logs(
|
||||
tenant_id: UUID,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
audit_service: AuditService = Depends(get_audit_service),
|
||||
current_user: Dict[str, Any] = Depends(require_admin)
|
||||
):
|
||||
"""Get audit logs for a tenant."""
|
||||
logs, total = await audit_service.get_by_tenant(
|
||||
tenant_id=str(tenant_id),
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [log.to_dict() for log in logs],
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset
|
||||
}
|
||||
Reference in New Issue
Block a user