""" 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 import re from datetime import datetime from typing import Optional, List, Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query, Header from pydantic import BaseModel from sqlalchemy.orm import Session from sqlalchemy import func 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 == True).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}}}}