feat: Vorbereitung-Module auf 100% — Compliance-Scope Backend, DELETE-Endpoints, Proxy-Fixes, blocked-content Tab
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
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
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>
This commit is contained in:
@@ -20,6 +20,7 @@ from .security_backlog_routes import router as security_backlog_router
|
||||
from .quality_routes import router as quality_router
|
||||
from .loeschfristen_routes import router as loeschfristen_router
|
||||
from .legal_template_routes import router as legal_template_router
|
||||
from .compliance_scope_routes import router as compliance_scope_router
|
||||
|
||||
# Include sub-routers
|
||||
router.include_router(audit_router)
|
||||
@@ -41,6 +42,7 @@ router.include_router(security_backlog_router)
|
||||
router.include_router(quality_router)
|
||||
router.include_router(loeschfristen_router)
|
||||
router.include_router(legal_template_router)
|
||||
router.include_router(compliance_scope_router)
|
||||
|
||||
__all__ = [
|
||||
"router",
|
||||
@@ -63,4 +65,5 @@ __all__ = [
|
||||
"quality_router",
|
||||
"loeschfristen_router",
|
||||
"legal_template_router",
|
||||
"compliance_scope_router",
|
||||
]
|
||||
|
||||
@@ -311,6 +311,42 @@ async def upsert_company_profile(
|
||||
db.close()
|
||||
|
||||
|
||||
@router.delete("", status_code=200)
|
||||
async def delete_company_profile(
|
||||
tenant_id: str = "default",
|
||||
x_tenant_id: Optional[str] = Header(None, alias="X-Tenant-ID"),
|
||||
):
|
||||
"""Delete company profile for a tenant (DSGVO Recht auf Loeschung, Art. 17)."""
|
||||
tid = x_tenant_id or tenant_id
|
||||
db = SessionLocal()
|
||||
try:
|
||||
existing = db.execute(
|
||||
"SELECT id FROM compliance_company_profiles WHERE tenant_id = :tid",
|
||||
{"tid": tid},
|
||||
).fetchone()
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="Company profile not found")
|
||||
|
||||
db.execute(
|
||||
"DELETE FROM compliance_company_profiles WHERE tenant_id = :tid",
|
||||
{"tid": tid},
|
||||
)
|
||||
|
||||
log_audit(db, tid, "delete", None, None)
|
||||
db.commit()
|
||||
|
||||
return {"success": True, "message": "Company profile deleted"}
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Failed to delete company profile: {e}")
|
||||
raise HTTPException(status_code=500, detail="Failed to delete company profile")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@router.get("/audit", response_model=AuditListResponse)
|
||||
async def get_audit_log(
|
||||
tenant_id: str = "default",
|
||||
|
||||
134
backend-compliance/compliance/api/compliance_scope_routes.py
Normal file
134
backend-compliance/compliance/api/compliance_scope_routes.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user