""" E-Mail-Template Routes — Benachrichtigungsvorlagen fuer DSGVO-Compliance. Verwaltet Templates fuer DSR, Consent, Breach, Vendor und Training E-Mails. Inklusive Versionierung, Approval-Workflow, Vorschau und Send-Logging. """ import uuid from datetime import datetime from typing import Optional, Dict from fastapi import APIRouter, Depends, HTTPException, Query, Header from pydantic import BaseModel from sqlalchemy.orm import Session from classroom_engine.database import get_db from ..db.email_template_models import ( EmailTemplateDB, EmailTemplateVersionDB, EmailTemplateApprovalDB, EmailSendLogDB, EmailTemplateSettingsDB, ) router = APIRouter(prefix="/email-templates", tags=["compliance-email-templates"]) DEFAULT_TENANT = "9282a473-5c95-4b3a-bf78-0ecc0ec71d3e" # Template-Typen und zugehoerige Variablen TEMPLATE_TYPES = { "welcome": {"name": "Willkommen", "category": "general", "variables": ["user_name", "company_name", "login_url"]}, "verification": {"name": "E-Mail-Verifizierung", "category": "general", "variables": ["user_name", "verification_url", "expiry_hours"]}, "password_reset": {"name": "Passwort zuruecksetzen", "category": "general", "variables": ["user_name", "reset_url", "expiry_hours"]}, "dsr_receipt": {"name": "DSR Eingangsbestaetigung", "category": "dsr", "variables": ["requester_name", "reference_number", "request_type", "deadline"]}, "dsr_identity_request": {"name": "DSR Identitaetsanfrage", "category": "dsr", "variables": ["requester_name", "reference_number"]}, "dsr_completion": {"name": "DSR Abschluss", "category": "dsr", "variables": ["requester_name", "reference_number", "request_type", "completion_date"]}, "dsr_rejection": {"name": "DSR Ablehnung", "category": "dsr", "variables": ["requester_name", "reference_number", "rejection_reason", "legal_basis"]}, "dsr_extension": {"name": "DSR Fristverlaengerung", "category": "dsr", "variables": ["requester_name", "reference_number", "new_deadline", "extension_reason"]}, "consent_request": {"name": "Einwilligungsanfrage", "category": "consent", "variables": ["user_name", "purpose", "consent_url"]}, "consent_confirmation": {"name": "Einwilligungsbestaetigung", "category": "consent", "variables": ["user_name", "purpose", "consent_date"]}, "consent_withdrawal": {"name": "Widerruf bestaetigt", "category": "consent", "variables": ["user_name", "purpose", "withdrawal_date"]}, "consent_reminder": {"name": "Einwilligungs-Erinnerung", "category": "consent", "variables": ["user_name", "purpose", "expiry_date"]}, "breach_notification_authority": {"name": "Datenpanne Aufsichtsbehoerde", "category": "breach", "variables": ["incident_date", "incident_description", "affected_count", "measures_taken", "authority_name"]}, "breach_notification_affected": {"name": "Datenpanne Betroffene", "category": "breach", "variables": ["user_name", "incident_date", "incident_description", "measures_taken", "contact_info"]}, "breach_internal": {"name": "Datenpanne intern", "category": "breach", "variables": ["reporter_name", "incident_date", "incident_description", "severity"]}, "vendor_dpa_request": {"name": "AVV-Anfrage", "category": "vendor", "variables": ["vendor_name", "contact_name", "deadline", "requirements"]}, "vendor_review_reminder": {"name": "Vendor-Pruefung Erinnerung", "category": "vendor", "variables": ["vendor_name", "review_due_date", "last_review_date"]}, "training_invitation": {"name": "Schulungseinladung", "category": "training", "variables": ["user_name", "training_title", "training_date", "training_url"]}, "training_reminder": {"name": "Schulungs-Erinnerung", "category": "training", "variables": ["user_name", "training_title", "deadline"]}, "training_completion": {"name": "Schulung abgeschlossen", "category": "training", "variables": ["user_name", "training_title", "completion_date", "certificate_url"]}, } VALID_STATUSES = ["draft", "review", "approved", "published"] VALID_CATEGORIES = ["general", "dsr", "consent", "breach", "vendor", "training"] # ============================================================================= # Pydantic Schemas # ============================================================================= class TemplateCreate(BaseModel): template_type: str name: Optional[str] = None description: Optional[str] = None category: Optional[str] = None is_active: bool = True class VersionCreate(BaseModel): version: str = "1.0" language: str = "de" subject: str body_html: str body_text: Optional[str] = None class VersionUpdate(BaseModel): subject: Optional[str] = None body_html: Optional[str] = None body_text: Optional[str] = None class PreviewRequest(BaseModel): variables: Optional[Dict[str, str]] = None class SendTestRequest(BaseModel): recipient: str variables: Optional[Dict[str, str]] = None class SettingsUpdate(BaseModel): sender_name: Optional[str] = None sender_email: Optional[str] = None reply_to: Optional[str] = None logo_url: Optional[str] = None primary_color: Optional[str] = None secondary_color: Optional[str] = None footer_text: Optional[str] = None company_name: Optional[str] = None company_address: Optional[str] = None # ============================================================================= # Helpers # ============================================================================= def _get_tenant(x_tenant_id: Optional[str] = Header(None, alias='X-Tenant-ID')) -> str: return x_tenant_id or DEFAULT_TENANT def _template_to_dict(t: EmailTemplateDB, latest_version=None) -> dict: result = { "id": str(t.id), "tenant_id": str(t.tenant_id), "template_type": t.template_type, "name": t.name, "description": t.description, "category": t.category, "is_active": t.is_active, "sort_order": t.sort_order, "variables": t.variables or [], "created_at": t.created_at.isoformat() if t.created_at else None, "updated_at": t.updated_at.isoformat() if t.updated_at else None, } if latest_version: result["latest_version"] = _version_to_dict(latest_version) return result def _version_to_dict(v: EmailTemplateVersionDB) -> dict: return { "id": str(v.id), "template_id": str(v.template_id), "version": v.version, "language": v.language, "subject": v.subject, "body_html": v.body_html, "body_text": v.body_text, "status": v.status, "submitted_at": v.submitted_at.isoformat() if v.submitted_at else None, "submitted_by": v.submitted_by, "published_at": v.published_at.isoformat() if v.published_at else None, "published_by": v.published_by, "created_at": v.created_at.isoformat() if v.created_at else None, "created_by": v.created_by, } def _render_template(html: str, variables: Dict[str, str]) -> str: """Replace {{variable}} placeholders with values.""" result = html for key, value in variables.items(): result = result.replace(f"{{{{{key}}}}}", str(value)) return result # ============================================================================= # Template Type Info (MUST be before parameterized routes) # ============================================================================= @router.get("/types") async def get_template_types(): """Gibt alle verfuegbaren Template-Typen mit Variablen zurueck.""" return [ { "type": ttype, "name": info["name"], "category": info["category"], "variables": info["variables"], } for ttype, info in TEMPLATE_TYPES.items() ] @router.get("/stats") async def get_stats( tenant_id: str = Depends(_get_tenant), db: Session = Depends(get_db), ): """Statistiken ueber E-Mail-Templates.""" tid = uuid.UUID(tenant_id) base = db.query(EmailTemplateDB).filter(EmailTemplateDB.tenant_id == tid) total = base.count() active = base.filter(EmailTemplateDB.is_active).count() # Count templates with published versions published_count = 0 templates = base.all() for t in templates: has_published = db.query(EmailTemplateVersionDB).filter( EmailTemplateVersionDB.template_id == t.id, EmailTemplateVersionDB.status == "published", ).count() > 0 if has_published: published_count += 1 # By category by_category = {} for cat in VALID_CATEGORIES: by_category[cat] = base.filter(EmailTemplateDB.category == cat).count() # Send logs stats total_sent = db.query(EmailSendLogDB).filter(EmailSendLogDB.tenant_id == tid).count() return { "total": total, "active": active, "published": published_count, "draft": total - published_count, "by_category": by_category, "total_sent": total_sent, } @router.get("/settings") async def get_settings( tenant_id: str = Depends(_get_tenant), db: Session = Depends(get_db), ): """Globale E-Mail-Einstellungen laden.""" tid = uuid.UUID(tenant_id) settings = db.query(EmailTemplateSettingsDB).filter( EmailTemplateSettingsDB.tenant_id == tid, ).first() if not settings: return { "sender_name": "Datenschutzbeauftragter", "sender_email": "datenschutz@example.de", "reply_to": None, "logo_url": None, "primary_color": "#4F46E5", "secondary_color": "#7C3AED", "footer_text": "Datenschutzhinweis: Diese E-Mail enthaelt vertrauliche Informationen.", "company_name": None, "company_address": None, } return { "sender_name": settings.sender_name, "sender_email": settings.sender_email, "reply_to": settings.reply_to, "logo_url": settings.logo_url, "primary_color": settings.primary_color, "secondary_color": settings.secondary_color, "footer_text": settings.footer_text, "company_name": settings.company_name, "company_address": settings.company_address, } @router.put("/settings") async def update_settings( body: SettingsUpdate, tenant_id: str = Depends(_get_tenant), db: Session = Depends(get_db), ): """Globale E-Mail-Einstellungen speichern.""" tid = uuid.UUID(tenant_id) settings = db.query(EmailTemplateSettingsDB).filter( EmailTemplateSettingsDB.tenant_id == tid, ).first() if not settings: settings = EmailTemplateSettingsDB(tenant_id=tid) db.add(settings) for field in ["sender_name", "sender_email", "reply_to", "logo_url", "primary_color", "secondary_color", "footer_text", "company_name", "company_address"]: val = getattr(body, field, None) if val is not None: setattr(settings, field, val) settings.updated_at = datetime.utcnow() db.commit() db.refresh(settings) return { "sender_name": settings.sender_name, "sender_email": settings.sender_email, "reply_to": settings.reply_to, "logo_url": settings.logo_url, "primary_color": settings.primary_color, "secondary_color": settings.secondary_color, "footer_text": settings.footer_text, "company_name": settings.company_name, "company_address": settings.company_address, } @router.get("/logs") async def get_send_logs( limit: int = Query(20, ge=1, le=100), offset: int = Query(0, ge=0), template_type: Optional[str] = Query(None), tenant_id: str = Depends(_get_tenant), db: Session = Depends(get_db), ): """Send-Logs (paginiert).""" tid = uuid.UUID(tenant_id) query = db.query(EmailSendLogDB).filter(EmailSendLogDB.tenant_id == tid) if template_type: query = query.filter(EmailSendLogDB.template_type == template_type) total = query.count() logs = query.order_by(EmailSendLogDB.sent_at.desc()).offset(offset).limit(limit).all() return { "logs": [ { "id": str(l.id), "template_type": l.template_type, "recipient": l.recipient, "subject": l.subject, "status": l.status, "variables": l.variables or {}, "error_message": l.error_message, "sent_at": l.sent_at.isoformat() if l.sent_at else None, } for l in logs ], "total": total, "limit": limit, "offset": offset, } @router.post("/initialize") async def initialize_defaults( tenant_id: str = Depends(_get_tenant), db: Session = Depends(get_db), ): """Default-Templates fuer einen Tenant initialisieren.""" tid = uuid.UUID(tenant_id) existing = db.query(EmailTemplateDB).filter(EmailTemplateDB.tenant_id == tid).count() if existing > 0: return {"message": "Templates already initialized", "count": existing} created = 0 for idx, (ttype, info) in enumerate(TEMPLATE_TYPES.items()): t = EmailTemplateDB( tenant_id=tid, template_type=ttype, name=info["name"], category=info["category"], sort_order=idx * 10, variables=info["variables"], ) db.add(t) created += 1 db.commit() return {"message": f"{created} templates created", "count": created} @router.get("/default/{template_type}") async def get_default_content(template_type: str): """Default-Content fuer einen Template-Typ.""" if template_type not in TEMPLATE_TYPES: raise HTTPException(status_code=404, detail=f"Unknown template type: {template_type}") info = TEMPLATE_TYPES[template_type] vars_html = " ".join([f'{{{{{v}}}}}' for v in info["variables"]]) return { "template_type": template_type, "name": info["name"], "category": info["category"], "variables": info["variables"], "default_subject": f"{info['name']} - {{{{company_name}}}}", "default_body_html": f"
Sehr geehrte(r) {{{{user_name}}}},
\n[Inhalt hier einfuegen]
\nVerfuegbare Variablen: {vars_html}
\nMit freundlichen Gruessen
{{{{sender_name}}}}