Audiofy – Musikwünsche für Partys

Server (Website, Admin-Bereich, Player-API) und Player (raspotify +
Controller) mit Install-Skripten für Debian-Systeme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-17 19:16:39 +01:00
co-authored by Claude Opus 5.5
commit 3610de7bae
45 changed files with 5666 additions and 0 deletions
+58
View File
@@ -0,0 +1,58 @@
"""Audiofy-Web – Cloudflare Turnstile: Bot-Schutz für Admin-Login und Wünsche.
Kein Domain-Umzug nötig: Der Browser holt sich bei Cloudflare ein Token, der
Server prüft es per siteverify. Ohne TURNSTILE_SITE_KEY/TURNSTILE_SECRET_KEY in
der .env ist der Schutz aus (z. B. in Tests).
Ist Cloudflare nicht erreichbar, bleibt der Admin-Login gesperrt (sicher),
Wünsche gehen dagegen durch (die Party soll nicht stillstehen) – mit Logeintrag.
"""
import logging
from urllib.parse import urlsplit
import httpx
from fastapi import HTTPException, Request
from .config import konfig
log = logging.getLogger("audiofy.turnstile")
VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
def aktiv() -> bool:
k = konfig()
return bool(k.turnstile_site_key and k.turnstile_secret)
def _siteverify(daten: dict) -> dict:
r = httpx.post(VERIFY_URL, data=daten, timeout=8)
r.raise_for_status()
return r.json()
def pruefen(token: str | None, request: Request, aktion: str, bei_ausfall_erlauben: bool) -> None:
"""Wirft HTTPException, wenn das Turnstile-Token fehlt oder ungültig ist."""
if not aktiv():
return
if not token:
raise HTTPException(400, "Bitte warte kurz, bis die Sicherheitsprüfung abgeschlossen ist.")
daten = {"secret": konfig().turnstile_secret, "response": token}
if request.client:
daten["remoteip"] = request.client.host
try:
ergebnis = _siteverify(daten)
except (httpx.HTTPError, ValueError) as e:
if bei_ausfall_erlauben:
log.warning("Turnstile nicht erreichbar (%s) – %s ausnahmsweise ohne Prüfung erlaubt",
type(e).__name__, aktion)
return
log.error("Turnstile nicht erreichbar (%s) – %s abgelehnt", type(e).__name__, aktion)
raise HTTPException(503, "Die Sicherheitsprüfung ist gerade nicht erreichbar – bitte gleich nochmal versuchen.")
erwarteter_host = urlsplit(konfig().public_url).hostname
if (not ergebnis.get("success")
or (erwarteter_host and ergebnis.get("hostname") not in (None, erwarteter_host))
or (ergebnis.get("action") and ergebnis["action"] != aktion)):
log.info("Turnstile abgelehnt (%s): %s", aktion, ergebnis.get("error-codes"))
raise HTTPException(403, "Die Sicherheitsprüfung ist fehlgeschlagen – bitte nochmal versuchen.")