From 3fae57a0e229b733ac3206bc77a6c6c438f19ef4 Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 15 May 2026 16:46:37 +0200 Subject: [PATCH] Feat: Kontaktformular im Impressum + /api/contact Endpoint ohne Auth (SW by-v986) --- backend/main.py | 4 +- backend/routes/contact.py | 52 ++++++++++++++ backend/static/js/app.js | 2 +- backend/static/js/pages/impressum.js | 104 ++++++++++++++++++++++++++- backend/static/sw.js | 2 +- 5 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 backend/routes/contact.py diff --git a/backend/main.py b/backend/main.py index 099f0b7..ae5e58c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -253,6 +253,7 @@ from routes.challenges import router as challenges_router from routes.gassi_zeiten import router as gassi_zeiten_router from routes.help import router as help_router from routes.feedback import router as feedback_router +from routes.contact import router as contact_router from routes.invoices import router as invoices_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) @@ -318,6 +319,7 @@ app.include_router(challenges_router, prefix="/api/challenges", ta app.include_router(gassi_zeiten_router, prefix="/api/gassi-zeiten", tags=["Gassi-Zeiten"]) app.include_router(help_router, prefix="/api/help", tags=["Hilfe/FAQ"]) app.include_router(feedback_router, prefix="/api/feedback", tags=["Feedback"]) +app.include_router(contact_router, prefix="/api/contact", tags=["Kontakt"]) app.include_router(invoices_router) @@ -408,7 +410,7 @@ async def serve_media(path: str, request: _Request): raise _HE(404, "Nicht gefunden.") return _media_response(filepath) -APP_VER = "985" # muss mit APP_VER in app.js übereinstimmen +APP_VER = "986" # muss mit APP_VER in app.js übereinstimmen @app.get("/.well-known/assetlinks.json") async def assetlinks(): diff --git a/backend/routes/contact.py b/backend/routes/contact.py new file mode 100644 index 0000000..ee33cf4 --- /dev/null +++ b/backend/routes/contact.py @@ -0,0 +1,52 @@ +""" +BAN YARO — Öffentliches Kontaktformular (kein Login erforderlich) +Für Impressum-Kontaktpflicht nach § 5 DDG. +""" + +from fastapi import APIRouter, Request +from pydantic import BaseModel, EmailStr, Field +from typing import Annotated + +from mailer import send_email, email_html +from ratelimit import check as rl_check + +router = APIRouter() + +CONTACT_MAIL = "hallo@banyaro.app" + + +class ContactIn(BaseModel): + name: Annotated[str, Field(min_length=2, max_length=100)] + email: EmailStr + subject: Annotated[str, Field(min_length=3, max_length=150)] + message: Annotated[str, Field(min_length=10, max_length=3000)] + + +@router.post("") +async def submit_contact(payload: ContactIn, request: Request): + rl_check(request, max_requests=3, window_seconds=3600, key=f"contact_{payload.email}") + + body = f""" +

Neue Kontaktanfrage über das Impressum-Formular:

+ + + + + + + +
Name{payload.name}
E-Mail{payload.email}
Betreff{payload.subject}
+
+{payload.message} +
""" + + plain = f"Kontakt von {payload.name} ({payload.email})\nBetreff: {payload.subject}\n\n{payload.message}" + + await send_email( + CONTACT_MAIL, + f"Kontakt: {payload.subject} — {payload.name}", + email_html(body), + plain, + ) + return {"ok": True} diff --git a/backend/static/js/app.js b/backend/static/js/app.js index 7f2f4f3..ad4311a 100644 --- a/backend/static/js/app.js +++ b/backend/static/js/app.js @@ -3,7 +3,7 @@ Router, State-Management, Navigation, Initialisierung. ============================================================ */ -const APP_VER = '985'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen +const APP_VER = '986'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt const IS_STAGING = location.hostname === 'staging.banyaro.app'; // Cache-Bust-Parameter nach Update-Reload sofort entfernen diff --git a/backend/static/js/pages/impressum.js b/backend/static/js/pages/impressum.js index baacd1a..e05776d 100644 --- a/backend/static/js/pages/impressum.js +++ b/backend/static/js/pages/impressum.js @@ -24,11 +24,58 @@ window.Page_impressum = (() => {

Kontakt

-

+

E-Mail: hallo@banyaro.app
- Wir antworten in der Regel innerhalb von 24 Stunden. + Oder nutze das Formular — wir antworten in der Regel innerhalb von 24 Stunden.

+ +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+ + +
@@ -72,7 +119,58 @@ window.Page_impressum = (() => { `; } + function _initContactForm(container) { + const form = container.querySelector('#contact-form'); + const statusEl = container.querySelector('#cf-status'); + const submitBtn = container.querySelector('#cf-submit'); + if (!form) return; + + form.addEventListener('submit', async e => { + e.preventDefault(); + const name = container.querySelector('#cf-name').value.trim(); + const email = container.querySelector('#cf-email').value.trim(); + const subject = container.querySelector('#cf-subject').value.trim(); + const message = container.querySelector('#cf-message').value.trim(); + + submitBtn.disabled = true; + submitBtn.textContent = 'Wird gesendet…'; + statusEl.style.display = 'none'; + + try { + const res = await fetch('/api/contact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, email, subject, message }), + }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err.detail || 'Fehler beim Senden.'); + } + statusEl.style.display = 'block'; + statusEl.style.background = 'var(--c-success-bg, #f0fdf4)'; + statusEl.style.color = 'var(--c-success, #16a34a)'; + statusEl.textContent = '✓ Nachricht gesendet — wir melden uns bald!'; + form.reset(); + } catch (err) { + statusEl.style.display = 'block'; + statusEl.style.background = '#fef2f2'; + statusEl.style.color = '#dc2626'; + statusEl.textContent = err.message || 'Fehler beim Senden. Bitte versuche es später erneut.'; + submitBtn.disabled = false; + submitBtn.textContent = 'Nachricht senden'; + } + }); + } + + const _origInit = init; + function refresh() {} - return { init, refresh }; + return { + init(container) { + _origInit(container); + _initContactForm(container); + }, + refresh + }; })(); diff --git a/backend/static/sw.js b/backend/static/sw.js index 27d268b..2f3405b 100644 --- a/backend/static/sw.js +++ b/backend/static/sw.js @@ -3,7 +3,7 @@ Offline-Cache + Push Notifications + Tile-Cache ============================================================ */ -const CACHE_VERSION = 'by-v985'; +const CACHE_VERSION = 'by-v986'; const CACHE_STATIC = `${CACHE_VERSION}-static`; const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten const CACHE_API = 'ban-yaro-api-v1'; // API-Response-Cache