Files
breakpilot-compliance/backend-compliance/compliance/api/compliance_scope_routes.py
Benjamin Admin dc0d38ea40
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 35s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 19s
feat: Vorbereitung-Module auf 100% — Compliance-Scope Backend, DELETE-Endpoints, Proxy-Fixes, blocked-content Tab
Paket A — Kritische Blocker:
- compliance_scope_routes.py: GET + POST UPSERT für sdk_states JSONB-Feld
- compliance/api/__init__.py: compliance_scope_router registriert
- import/route.ts: POST-Proxy für multipart/form-data Upload
- screening/route.ts: POST-Proxy für Dependency-File Upload

Paket B — Backend + UI:
- company_profile_routes.py: DELETE-Endpoint (DSGVO Art. 17)
- company-profile/route.ts: DELETE-Proxy
- company-profile/page.tsx: Profil-löschen-Button mit Bestätigungs-Dialog
- source-policy/pii-rules/[id]/route.ts: GET ergänzt
- source-policy/operations/[id]/route.ts: GET + DELETE ergänzt

Paket C — Tests + UI:
- test_compliance_scope_routes.py: 27 Tests (neu)
- test_import_routes.py: +36 Tests → 60 gesamt
- test_screening_routes.py: +28 Tests → 80+ gesamt
- source-policy/page.tsx: "Blockierte Inhalte" Tab mit Tabelle + Remove

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 17:43:29 +01:00

135 lines
4.2 KiB
Python

"""
FastAPI routes for Compliance Scope persistence.
Stores the tenant's scope decision (frameworks, regulations, industry context)
in sdk_states.state->compliance_scope as JSONB.
Endpoints:
- GET /v1/compliance-scope?tenant_id=... → returns scope or 404
- POST /v1/compliance-scope → UPSERT scope (idempotent)
"""
import json
import logging
from typing import Any, Optional
from fastapi import APIRouter, HTTPException, Header
from pydantic import BaseModel
from database import SessionLocal
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1/compliance-scope", tags=["compliance-scope"])
# =============================================================================
# REQUEST / RESPONSE MODELS
# =============================================================================
class ComplianceScopeRequest(BaseModel):
"""Scope selection submitted by the frontend wizard."""
scope: dict[str, Any]
tenant_id: Optional[str] = None
class ComplianceScopeResponse(BaseModel):
"""Persisted scope object returned to the frontend."""
tenant_id: str
scope: dict[str, Any]
updated_at: str
created_at: str
# =============================================================================
# HELPERS
# =============================================================================
def _get_tid(
x_tenant_id: Optional[str],
query_tenant_id: str,
) -> str:
return x_tenant_id or query_tenant_id or "default"
def _row_to_response(row) -> ComplianceScopeResponse:
"""Convert a DB row (tenant_id, scope, created_at, updated_at) to response."""
return ComplianceScopeResponse(
tenant_id=row[0],
scope=row[1] if isinstance(row[1], dict) else {},
created_at=str(row[2]),
updated_at=str(row[3]),
)
# =============================================================================
# ROUTES
# =============================================================================
@router.get("", response_model=ComplianceScopeResponse)
async def get_compliance_scope(
tenant_id: str = "default",
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
):
"""Return the persisted compliance scope for a tenant, or 404 if not set."""
tid = _get_tid(x_tenant_id, tenant_id)
db = SessionLocal()
try:
row = db.execute(
"""SELECT tenant_id,
state->'compliance_scope' AS scope,
created_at,
updated_at
FROM sdk_states
WHERE tenant_id = :tid
AND state ? 'compliance_scope'""",
{"tid": tid},
).fetchone()
if not row or row[1] is None:
raise HTTPException(status_code=404, detail="Compliance scope not found")
return _row_to_response(row)
finally:
db.close()
@router.post("", response_model=ComplianceScopeResponse)
async def upsert_compliance_scope(
body: ComplianceScopeRequest,
tenant_id: str = "default",
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
):
"""Create or update the compliance scope for a tenant (UPSERT)."""
tid = _get_tid(x_tenant_id, body.tenant_id or tenant_id)
scope_json = json.dumps(body.scope)
db = SessionLocal()
try:
db.execute(
"""INSERT INTO sdk_states (tenant_id, state)
VALUES (:tid, jsonb_build_object('compliance_scope', :scope::jsonb))
ON CONFLICT (tenant_id) DO UPDATE
SET state = sdk_states.state || jsonb_build_object('compliance_scope', :scope::jsonb),
updated_at = NOW()""",
{"tid": tid, "scope": scope_json},
)
db.commit()
row = db.execute(
"""SELECT tenant_id,
state->'compliance_scope' AS scope,
created_at,
updated_at
FROM sdk_states
WHERE tenant_id = :tid""",
{"tid": tid},
).fetchone()
return _row_to_response(row)
except Exception as e:
db.rollback()
logger.error(f"Failed to upsert compliance scope: {e}")
raise HTTPException(status_code=500, detail="Failed to save compliance scope")
finally:
db.close()