"""Feedback-Router: Nutzer-Feedback per E-Mail an das Team.""" import html import time import logging from collections import defaultdict from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.base import MIMEBase from email import encoders from typing import List import aiosmtplib from fastapi import APIRouter, Depends, HTTPException, status, Form, UploadFile, File from auth import get_current_user from config import ( SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, SMTP_FROM_EMAIL, SMTP_FROM_NAME, SMTP_USE_TLS, ) logger = logging.getLogger("osint.feedback") router = APIRouter(prefix="/api", tags=["feedback"]) FEEDBACK_EMAIL = "feedback@aegis-sight.de" CATEGORY_LABELS = { "bug": "Fehlerbericht", "feature": "Feature-Wunsch", "question": "Frage", "other": "Sonstiges", } # In-Memory Rate-Limiting: max 3 pro Nutzer/Stunde _user_timestamps: dict[int, list[float]] = defaultdict(list) _MAX_PER_HOUR = 3 _WINDOW = 3600 _ALLOWED_TYPES = {"image/jpeg", "image/png"} _MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB _MAX_FILES = 3 @router.post("/feedback", status_code=204) async def send_feedback( category: str = Form(...), message: str = Form(..., min_length=10, max_length=5000), files: List[UploadFile] = File(default=[]), current_user: dict = Depends(get_current_user), ): """Feedback per E-Mail an das Team senden (mit optionalen Bild-Anhaengen).""" # Kategorie validieren if category not in CATEGORY_LABELS: raise HTTPException(status_code=422, detail="Ungueltige Kategorie.") user_id = current_user["id"] # Rate-Limiting now = time.time() cutoff = now - _WINDOW _user_timestamps[user_id] = [t for t in _user_timestamps[user_id] if t > cutoff] if len(_user_timestamps[user_id]) >= _MAX_PER_HOUR: raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Maximal 3 Feedback-Nachrichten pro Stunde. Bitte spaeter erneut versuchen.", ) # Dateien validieren if len(files) > _MAX_FILES: raise HTTPException(status_code=422, detail=f"Maximal {_MAX_FILES} Dateien erlaubt.") for f in files: if f.content_type not in _ALLOWED_TYPES: raise HTTPException(status_code=422, detail=f"Dateityp {f.content_type} nicht erlaubt (nur JPEG/PNG).") if not SMTP_HOST: logger.warning("SMTP nicht konfiguriert - Feedback nicht gesendet") raise HTTPException( status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="E-Mail-Versand nicht verfuegbar.", ) email = current_user.get("email", "") display_name = email.split("@")[0] if email else "Unbekannt" category_label = CATEGORY_LABELS.get(category, category) message_escaped = html.escape(message) subject = f"[AegisSight Feedback] {category_label} von {display_name}" html_body = f"""\

Neues Feedback

Kategorie:{category_label}
Nutzer:{html.escape(display_name)}
E-Mail:{html.escape(email) if email else "nicht hinterlegt"}

{message_escaped}
""" msg = MIMEMultipart("mixed") msg["From"] = f"{SMTP_FROM_NAME} <{SMTP_FROM_EMAIL}>" msg["To"] = FEEDBACK_EMAIL msg["Subject"] = subject if email: msg["Reply-To"] = email # Text-Teil (alternative: plain + html) text_part = MIMEMultipart("alternative") text_fallback = f"Feedback von {display_name} ({category_label}):\n\n{message}" text_part.attach(MIMEText(text_fallback, "plain", "utf-8")) text_part.attach(MIMEText(html_body, "html", "utf-8")) msg.attach(text_part) # Bild-Anhaenge for f in files: file_data = await f.read() if len(file_data) > _MAX_FILE_SIZE: raise HTTPException(status_code=422, detail=f"Datei {f.filename} ist groesser als 5 MB.") sub_type = (f.content_type or "image/jpeg").split("/")[1] attachment = MIMEBase("image", sub_type) attachment.set_payload(file_data) encoders.encode_base64(attachment) attachment.add_header("Content-Disposition", "attachment", filename=f.filename or "bild.jpg") msg.attach(attachment) logger.debug(f"Anhang: {f.filename} ({len(file_data)} Bytes, {f.content_type})") try: await aiosmtplib.send( msg, hostname=SMTP_HOST, port=SMTP_PORT, username=SMTP_USER if SMTP_USER else None, password=SMTP_PASSWORD if SMTP_PASSWORD else None, start_tls=SMTP_USE_TLS, ) _user_timestamps[user_id].append(now) logger.info(f"Feedback von {display_name} ({category_label}) gesendet, {len(files)} Anhaenge") except Exception as e: logger.error(f"Feedback-E-Mail fehlgeschlagen: {e}") raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="E-Mail konnte nicht gesendet werden.", )