Some checks failed
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) Failing after 30s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 17s
- Ruff: 144 auto-fixes (unused imports, == None → is None), F821/F811/F841 manuell - CVEs: python-multipart>=0.0.22, weasyprint>=68.0, pillow>=12.1.1, npm audit fix (0 vulns) - TS: 5 tote Drafting-Engine-Dateien entfernt, allowed-facts/sanitizer/StepHeader/context fixes - Tests: +104 (ISMS 58, Evidence 18, VVT 14, Generation 14) → 1449 passed - Refactoring: collect_ci_evidence (F→A), row_to_response (E→A), extract_requirements (E→A) - Dead Code: pca-platform, 7 Go-Handler, dsr_api.py, duplicate Schemas entfernt Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
314 lines
10 KiB
Python
314 lines
10 KiB
Python
"""
|
|
FastAPI routes for Consent Email Templates + DSGVO Processes.
|
|
|
|
Endpoints:
|
|
GET /consent-templates — List email templates (filtered by tenant)
|
|
POST /consent-templates — Create a new email template
|
|
PUT /consent-templates/{id} — Update an email template
|
|
DELETE /consent-templates/{id} — Delete an email template
|
|
GET /gdpr-processes — List GDPR processes
|
|
PUT /gdpr-processes/{id} — Update a GDPR process
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Header
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import text
|
|
|
|
from classroom_engine.database import get_db
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["consent-templates"])
|
|
|
|
|
|
# ============================================================================
|
|
# Pydantic Schemas
|
|
# ============================================================================
|
|
|
|
class ConsentTemplateCreate(BaseModel):
|
|
template_key: str
|
|
subject: str
|
|
body: str
|
|
language: str = 'de'
|
|
is_active: bool = True
|
|
|
|
|
|
class ConsentTemplateUpdate(BaseModel):
|
|
subject: Optional[str] = None
|
|
body: Optional[str] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
class GDPRProcessUpdate(BaseModel):
|
|
title: Optional[str] = None
|
|
description: Optional[str] = None
|
|
legal_basis: Optional[str] = None
|
|
retention_days: Optional[int] = None
|
|
is_active: Optional[bool] = None
|
|
|
|
|
|
# ============================================================================
|
|
# Helpers
|
|
# ============================================================================
|
|
|
|
def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias='X-Tenant-ID')) -> str:
|
|
return x_tenant_id or 'default'
|
|
|
|
|
|
# ============================================================================
|
|
# Email Templates
|
|
# ============================================================================
|
|
|
|
@router.get("/consent-templates")
|
|
async def list_consent_templates(
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
"""List all email templates for a tenant."""
|
|
rows = db.execute(
|
|
text("""
|
|
SELECT id, tenant_id, template_key, subject, body, language, is_active, created_at, updated_at
|
|
FROM compliance_consent_email_templates
|
|
WHERE tenant_id = :tenant_id
|
|
ORDER BY template_key, language
|
|
"""),
|
|
{"tenant_id": tenant_id},
|
|
).fetchall()
|
|
|
|
return [
|
|
{
|
|
"id": str(r.id),
|
|
"tenant_id": r.tenant_id,
|
|
"template_key": r.template_key,
|
|
"subject": r.subject,
|
|
"body": r.body,
|
|
"language": r.language,
|
|
"is_active": r.is_active,
|
|
"created_at": r.created_at.isoformat() if r.created_at else None,
|
|
"updated_at": r.updated_at.isoformat() if r.updated_at else None,
|
|
}
|
|
for r in rows
|
|
]
|
|
|
|
|
|
@router.post("/consent-templates", status_code=201)
|
|
async def create_consent_template(
|
|
request: ConsentTemplateCreate,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
"""Create a new email template."""
|
|
existing = db.execute(
|
|
text("""
|
|
SELECT id FROM compliance_consent_email_templates
|
|
WHERE tenant_id = :tenant_id AND template_key = :template_key AND language = :language
|
|
"""),
|
|
{"tenant_id": tenant_id, "template_key": request.template_key, "language": request.language},
|
|
).fetchone()
|
|
|
|
if existing:
|
|
raise HTTPException(
|
|
status_code=409,
|
|
detail=f"Template '{request.template_key}' for language '{request.language}' already exists for this tenant",
|
|
)
|
|
|
|
row = db.execute(
|
|
text("""
|
|
INSERT INTO compliance_consent_email_templates
|
|
(tenant_id, template_key, subject, body, language, is_active)
|
|
VALUES (:tenant_id, :template_key, :subject, :body, :language, :is_active)
|
|
RETURNING id, tenant_id, template_key, subject, body, language, is_active, created_at, updated_at
|
|
"""),
|
|
{
|
|
"tenant_id": tenant_id,
|
|
"template_key": request.template_key,
|
|
"subject": request.subject,
|
|
"body": request.body,
|
|
"language": request.language,
|
|
"is_active": request.is_active,
|
|
},
|
|
).fetchone()
|
|
db.commit()
|
|
|
|
return {
|
|
"id": str(row.id),
|
|
"tenant_id": row.tenant_id,
|
|
"template_key": row.template_key,
|
|
"subject": row.subject,
|
|
"body": row.body,
|
|
"language": row.language,
|
|
"is_active": row.is_active,
|
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
|
}
|
|
|
|
|
|
@router.put("/consent-templates/{template_id}")
|
|
async def update_consent_template(
|
|
template_id: str,
|
|
request: ConsentTemplateUpdate,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
"""Update an existing email template."""
|
|
existing = db.execute(
|
|
text("""
|
|
SELECT id FROM compliance_consent_email_templates
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""),
|
|
{"id": template_id, "tenant_id": tenant_id},
|
|
).fetchone()
|
|
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
|
|
|
updates = request.dict(exclude_none=True)
|
|
if not updates:
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
|
|
|
set_clauses = ", ".join(f"{k} = :{k}" for k in updates)
|
|
updates["id"] = template_id
|
|
updates["tenant_id"] = tenant_id
|
|
updates["now"] = datetime.utcnow()
|
|
|
|
row = db.execute(
|
|
text(f"""
|
|
UPDATE compliance_consent_email_templates
|
|
SET {set_clauses}, updated_at = :now
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING id, tenant_id, template_key, subject, body, language, is_active, created_at, updated_at
|
|
"""),
|
|
updates,
|
|
).fetchone()
|
|
db.commit()
|
|
|
|
return {
|
|
"id": str(row.id),
|
|
"tenant_id": row.tenant_id,
|
|
"template_key": row.template_key,
|
|
"subject": row.subject,
|
|
"body": row.body,
|
|
"language": row.language,
|
|
"is_active": row.is_active,
|
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
"updated_at": row.updated_at.isoformat() if row.updated_at else None,
|
|
}
|
|
|
|
|
|
@router.delete("/consent-templates/{template_id}")
|
|
async def delete_consent_template(
|
|
template_id: str,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
"""Delete an email template."""
|
|
existing = db.execute(
|
|
text("""
|
|
SELECT id FROM compliance_consent_email_templates
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""),
|
|
{"id": template_id, "tenant_id": tenant_id},
|
|
).fetchone()
|
|
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail=f"Template {template_id} not found")
|
|
|
|
db.execute(
|
|
text("DELETE FROM compliance_consent_email_templates WHERE id = :id AND tenant_id = :tenant_id"),
|
|
{"id": template_id, "tenant_id": tenant_id},
|
|
)
|
|
db.commit()
|
|
|
|
return {"success": True, "message": f"Template {template_id} deleted"}
|
|
|
|
|
|
# ============================================================================
|
|
# GDPR Processes
|
|
# ============================================================================
|
|
|
|
@router.get("/gdpr-processes")
|
|
async def list_gdpr_processes(
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
"""List all GDPR processes for a tenant."""
|
|
rows = db.execute(
|
|
text("""
|
|
SELECT id, tenant_id, process_key, title, description, legal_basis, retention_days, is_active, created_at
|
|
FROM compliance_consent_gdpr_processes
|
|
WHERE tenant_id = :tenant_id
|
|
ORDER BY process_key
|
|
"""),
|
|
{"tenant_id": tenant_id},
|
|
).fetchall()
|
|
|
|
return [
|
|
{
|
|
"id": str(r.id),
|
|
"tenant_id": r.tenant_id,
|
|
"process_key": r.process_key,
|
|
"title": r.title,
|
|
"description": r.description,
|
|
"legal_basis": r.legal_basis,
|
|
"retention_days": r.retention_days,
|
|
"is_active": r.is_active,
|
|
"created_at": r.created_at.isoformat() if r.created_at else None,
|
|
}
|
|
for r in rows
|
|
]
|
|
|
|
|
|
@router.put("/gdpr-processes/{process_id}")
|
|
async def update_gdpr_process(
|
|
process_id: str,
|
|
request: GDPRProcessUpdate,
|
|
db: Session = Depends(get_db),
|
|
tenant_id: str = Depends(_get_tenant),
|
|
):
|
|
"""Update an existing GDPR process."""
|
|
existing = db.execute(
|
|
text("""
|
|
SELECT id FROM compliance_consent_gdpr_processes
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
"""),
|
|
{"id": process_id, "tenant_id": tenant_id},
|
|
).fetchone()
|
|
|
|
if not existing:
|
|
raise HTTPException(status_code=404, detail=f"GDPR process {process_id} not found")
|
|
|
|
updates = request.dict(exclude_none=True)
|
|
if not updates:
|
|
raise HTTPException(status_code=400, detail="No fields to update")
|
|
|
|
set_clauses = ", ".join(f"{k} = :{k}" for k in updates)
|
|
updates["id"] = process_id
|
|
updates["tenant_id"] = tenant_id
|
|
|
|
row = db.execute(
|
|
text(f"""
|
|
UPDATE compliance_consent_gdpr_processes
|
|
SET {set_clauses}
|
|
WHERE id = :id AND tenant_id = :tenant_id
|
|
RETURNING id, tenant_id, process_key, title, description, legal_basis, retention_days, is_active, created_at
|
|
"""),
|
|
updates,
|
|
).fetchone()
|
|
db.commit()
|
|
|
|
return {
|
|
"id": str(row.id),
|
|
"tenant_id": row.tenant_id,
|
|
"process_key": row.process_key,
|
|
"title": row.title,
|
|
"description": row.description,
|
|
"legal_basis": row.legal_basis,
|
|
"retention_days": row.retention_days,
|
|
"is_active": row.is_active,
|
|
"created_at": row.created_at.isoformat() if row.created_at else None,
|
|
}
|