refactor: form-core wiring, JWT sessions, slowapi, packaging, frontend rewrite, test declutter
This commit is contained in:
parent
06ab25fb47
commit
03af41e837
74 changed files with 6089 additions and 5183 deletions
|
|
@ -9,8 +9,10 @@ fonts, no external images (many clients block them / track opens).
|
|||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import smtplib
|
||||
from email.message import EmailMessage
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import Template
|
||||
|
||||
from app.settings import settings
|
||||
|
||||
|
|
@ -22,67 +24,62 @@ BLUE_LIGHT = "#eef4fb"
|
|||
INK = "#162f4c"
|
||||
MUTED = "#5b6b7c"
|
||||
|
||||
_TEMPLATE_PATH = Path(__file__).resolve().parent / "templates" / "magic_link.html"
|
||||
_template_cache: Template | None = None
|
||||
|
||||
|
||||
def _template() -> Template:
|
||||
global _template_cache
|
||||
if _template_cache is None:
|
||||
_template_cache = Template(_TEMPLATE_PATH.read_text(encoding="utf-8"))
|
||||
return _template_cache
|
||||
|
||||
|
||||
def magic_link(token: str) -> str:
|
||||
return f"{settings.frontend_url.rstrip('/')}/login?token={token}"
|
||||
|
||||
|
||||
def _page(title: str, intro: str, button_label: str, button_url: str | None, note: str) -> str:
|
||||
"""Branded HTML mail body (table layout, inline styles, no images)."""
|
||||
button = ""
|
||||
if button_url:
|
||||
button = (
|
||||
'<table role="presentation" cellpadding="0" cellspacing="0" '
|
||||
'style="margin:24px auto 8px auto;"><tr>'
|
||||
f'<td bgcolor="{BLUE}" style="border-radius:8px;">'
|
||||
f'<a href="{button_url}" '
|
||||
'style="display:inline-block;padding:12px 28px;font-family:-apple-system,'
|
||||
"Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:15px;"
|
||||
"font-weight:600;color:#ffffff;text-decoration:none;"
|
||||
f'border-radius:8px;">{button_label}</a></td></tr></table>'
|
||||
)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<body style="margin:0;padding:0;background-color:{BLUE_LIGHT};">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"
|
||||
style="background-color:{BLUE_LIGHT};padding:24px 12px;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="520" cellpadding="0" cellspacing="0"
|
||||
style="max-width:520px;width:100%;font-family:Georgia,'Times New Roman',serif;
|
||||
border-radius:12px;overflow:hidden;border:1px solid #d9e6f5;">
|
||||
<tr>
|
||||
<td bgcolor="{BLUE}" style="padding:20px 28px;">
|
||||
<span style="color:#ffffff;font-size:18px;font-weight:700;
|
||||
letter-spacing:0.02em;">Kirche Einfach</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" style="padding:28px 28px 24px 28px;">
|
||||
<h1 style="margin:0 0 12px 0;font-size:22px;line-height:1.3;
|
||||
color:{INK};">{title}</h1>
|
||||
<p style="margin:0;font-family:-apple-system,Segoe UI,Roboto,
|
||||
Helvetica,Arial,sans-serif;font-size:15px;line-height:1.6;
|
||||
color:{MUTED};">{intro}</p>
|
||||
{button}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
|
||||
font-size:12px;color:{MUTED};margin:16px 0 0 0;max-width:520px;">
|
||||
{note}
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
"""Branded HTML mail body (jinja template, byte-identical to the old f-string)."""
|
||||
return _template().render(
|
||||
title=title,
|
||||
intro=intro,
|
||||
button_label=button_label,
|
||||
button_url=button_url,
|
||||
note=note,
|
||||
blue=BLUE,
|
||||
blue_light=BLUE_LIGHT,
|
||||
ink=INK,
|
||||
muted=MUTED,
|
||||
)
|
||||
|
||||
|
||||
def _send(message: EmailMessage) -> None:
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=10) as smtp:
|
||||
smtp.starttls()
|
||||
if settings.smtp_username:
|
||||
smtp.login(settings.smtp_username, settings.smtp_password)
|
||||
smtp.send_message(message)
|
||||
"""Deliver via fastapi-mail (same never-raise contract at call sites)."""
|
||||
import asyncio
|
||||
|
||||
from fastapi_mail import ConnectionConfig, FastMail, MessageSchema, MessageType
|
||||
|
||||
config = ConnectionConfig(
|
||||
MAIL_USERNAME=settings.smtp_username,
|
||||
MAIL_PASSWORD=settings.smtp_password,
|
||||
MAIL_FROM=settings.mail_from or settings.contact_to or message["To"],
|
||||
MAIL_PORT=settings.smtp_port,
|
||||
MAIL_SERVER=settings.smtp_host,
|
||||
MAIL_STARTTLS=True,
|
||||
MAIL_SSL_TLS=False,
|
||||
USE_CREDENTIALS=bool(settings.smtp_username),
|
||||
)
|
||||
html = message.get_body(preferencelist=("html",))
|
||||
plain = message.get_body(preferencelist=("plain",))
|
||||
outgoing = MessageSchema(
|
||||
subject=str(message["Subject"]),
|
||||
recipients=[str(message["To"])],
|
||||
body=plain.get_content() if plain else "",
|
||||
html=html.get_content() if html else "",
|
||||
subtype=MessageType.html,
|
||||
)
|
||||
asyncio.run(FastMail(config).send_message(outgoing))
|
||||
|
||||
|
||||
def _branded_mail(
|
||||
|
|
@ -142,7 +139,7 @@ def send_magic_link(to_email: str, link: str) -> bool:
|
|||
)
|
||||
try:
|
||||
_send(message)
|
||||
except (OSError, smtplib.SMTPException):
|
||||
except Exception: # noqa: BLE001 -- never-raise contract: delivery must not 500
|
||||
logger.warning("magic-link delivery failed for %s", to_email)
|
||||
return False
|
||||
logger.info("magic link mailed to %s", to_email)
|
||||
|
|
@ -167,7 +164,7 @@ def send_access_notification(request_id: str, name: str, email: str) -> bool:
|
|||
)
|
||||
try:
|
||||
_send(message)
|
||||
except (OSError, smtplib.SMTPException):
|
||||
except Exception: # noqa: BLE001 -- best-effort notification, never raises
|
||||
logger.warning("access-request notification failed for %s", request_id)
|
||||
return False
|
||||
return True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue