Sprint 15: User-Profil (Bio, Wohnort, Erfahrung, Social, Avatar, Mitglied-seit)

This commit is contained in:
rene 2026-04-17 09:24:27 +02:00
parent 3642995409
commit 596c11d207
6 changed files with 189 additions and 1 deletions

148
backend/mailer.py Normal file
View file

@ -0,0 +1,148 @@
"""
BAN YARO E-Mail-Versand
Unterstützt zwei Backends (wird automatisch gewählt):
1. Brevo REST-API wenn BREVO_API_KEY gesetzt (bevorzugt)
2. SMTP wenn SMTP_HOST gesetzt (Fallback)
"""
import os
import smtplib
import asyncio
import logging
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import httpx
logger = logging.getLogger(__name__)
# Brevo REST-API
BREVO_API_KEY = os.getenv("BREVO_API_KEY", "")
BREVO_API_URL = "https://api.brevo.com/v3/smtp/email"
# SMTP Fallback
SMTP_HOST = os.getenv("SMTP_HOST", "")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
SMTP_USER = os.getenv("SMTP_USER", "")
SMTP_PASS = os.getenv("SMTP_PASS", "")
SMTP_FROM = os.getenv("SMTP_FROM", "Ban Yaro <noreply@banyaro.app>")
APP_URL = os.getenv("APP_URL", "https://banyaro.app")
# ------------------------------------------------------------------
# Brevo REST-API
# ------------------------------------------------------------------
async def _send_brevo(to: str, subject: str, html: str, plain: str):
# Absender-Name und -Adresse aus SMTP_FROM parsen
# Format: "Ban Yaro <noreply@banyaro.app>" oder "noreply@banyaro.app"
from_raw = SMTP_FROM
if "<" in from_raw:
from_name = from_raw[:from_raw.index("<")].strip()
from_email = from_raw[from_raw.index("<")+1:from_raw.index(">")].strip()
else:
from_name = "Ban Yaro"
from_email = from_raw.strip()
payload = {
"sender": {"name": from_name, "email": from_email},
"to": [{"email": to}],
"subject": subject,
"htmlContent": html,
"textContent": plain,
}
async with httpx.AsyncClient(timeout=15) as client:
resp = await client.post(
BREVO_API_URL,
json=payload,
headers={"api-key": BREVO_API_KEY, "Content-Type": "application/json"},
)
resp.raise_for_status()
# ------------------------------------------------------------------
# SMTP Fallback
# ------------------------------------------------------------------
def _send_smtp_sync(to: str, subject: str, html: str, plain: str):
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = SMTP_FROM
msg["To"] = to
msg.attach(MIMEText(plain, "plain", "utf-8"))
msg.attach(MIMEText(html, "html", "utf-8"))
with smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=10) as s:
s.ehlo()
s.starttls()
if SMTP_USER:
s.login(SMTP_USER, SMTP_PASS)
s.sendmail(SMTP_FROM, [to], msg.as_string())
# ------------------------------------------------------------------
# Öffentliche Funktion
# ------------------------------------------------------------------
async def send_email(to: str, subject: str, html: str, plain: str = ""):
if BREVO_API_KEY:
try:
await _send_brevo(to, subject, html, plain)
logger.info(f"Mail via Brevo gesendet: «{subject}» → {to}")
return
except Exception as e:
logger.error(f"Brevo-Fehler: {e}")
raise
if SMTP_HOST:
loop = asyncio.get_event_loop()
try:
await loop.run_in_executor(None, _send_smtp_sync, to, subject, html, plain)
logger.info(f"Mail via SMTP gesendet: «{subject}» → {to}")
return
except Exception as e:
logger.error(f"SMTP-Fehler: {e}")
raise
logger.warning(f"Kein Mail-Backend konfiguriert — Mail NICHT gesendet: «{subject}» → {to}")
async def send_verify_email(to: str, name: str, token: str):
url = f"{APP_URL}/api/auth/verify/{token}"
subject = "Ban Yaro — E-Mail-Adresse bestätigen"
html = f"""\
<!DOCTYPE html>
<html lang="de">
<head><meta charset="utf-8"></head>
<body style="font-family:sans-serif;background:#f9f9f9;margin:0;padding:0">
<div style="max-width:520px;margin:32px auto;background:#fff;border-radius:12px;
padding:40px 32px;box-shadow:0 2px 8px rgba(0,0,0,.08)">
<h1 style="color:#C4843A;margin:0 0 8px;font-size:24px">Ban Yaro 🐾</h1>
<p style="color:#444;margin:0 0 24px">Hallo {name},</p>
<p style="color:#444;margin:0 0 24px">
bitte bestätige deine E-Mail-Adresse, damit dein Konto vollständig eingerichtet ist.
</p>
<p style="margin:0 0 32px">
<a href="{url}"
style="background:#C4843A;color:#fff;padding:14px 28px;border-radius:8px;
text-decoration:none;font-weight:700;font-size:15px;display:inline-block">
E-Mail bestätigen
</a>
</p>
<p style="color:#888;font-size:13px;margin:0 0 8px">
Der Link ist 48 Stunden gültig.
</p>
<p style="color:#bbb;font-size:12px;margin:0">
Falls du dich nicht bei Ban Yaro registriert hast, ignoriere diese E-Mail einfach.
</p>
</div>
</body>
</html>"""
plain = (
f"Ban Yaro — E-Mail-Adresse bestätigen\n\n"
f"Hallo {name},\n\n"
f"bitte bestätige deine E-Mail-Adresse:\n{url}\n\n"
f"Der Link ist 48 Stunden gültig.\n"
)
await send_email(to, subject, html, plain)