Fix: Forum-Cooldown blockierte JEDEN Post (Zeitzonen-Bug)

_check_post_limits verglich datetime.utcnow() (UTC) mit created_at, das aber
als Client-Lokalzeit (CEST=UTC+2) gespeichert wird → diff negativ → immer 429.
Jetzt rechnen Cooldown + Stunden-Limit in derselben Zeitbasis (Client-Zeit des
Requests). Backend-only, kein SW-Bump.
This commit is contained in:
rene 2026-06-04 16:50:45 +02:00
parent c07b1cc01b
commit 959fd81a9b

View file

@ -166,8 +166,22 @@ async def list_threads(
# ------------------------------------------------------------------
# POST /api/forum/threads
# ------------------------------------------------------------------
def _check_post_limits(user_id: int, conn, text: str, user_created_at: str | None = None, is_thread: bool = False):
"""Cooldown, Stunden-Limit und Duplikat-Check für Forum-Posts."""
def _check_post_limits(user_id: int, conn, text: str, user_created_at: str | None = None,
is_thread: bool = False, now_client: str | None = None):
"""Cooldown, Stunden-Limit und Duplikat-Check für Forum-Posts.
WICHTIG: created_at wird als Client-Lokalzeit gespeichert (safe_client_time).
Alle Zeit-Checks müssen daher gegen die gleiche Zeitbasis rechnen sonst
sorgt der UTC/Lokalzeit-Versatz (z.B. CEST = UTC+2) dafür, dass der Cooldown
dauerhaft greift (diff wird negativ immer < 30). Referenz ist die
Client-Zeit dieses Requests (now_client), Fallback UTC.
"""
from datetime import datetime as _dt, timedelta as _td
try:
now_dt = _dt.fromisoformat(now_client) if now_client else _dt.utcnow()
except (ValueError, TypeError):
now_dt = _dt.utcnow()
# 30-Sekunden-Cooldown zwischen beliebigen Posts
last = conn.execute(
"""SELECT MAX(created_at) AS last FROM (
@ -179,25 +193,25 @@ def _check_post_limits(user_id: int, conn, text: str, user_created_at: str | Non
).fetchone()["last"]
if last:
try:
from datetime import datetime as _dt
diff = (_dt.utcnow() - _dt.fromisoformat(last)).total_seconds()
if diff < 30:
diff = (now_dt - _dt.fromisoformat(last)).total_seconds()
if 0 <= diff < 30:
raise HTTPException(429, "Bitte warte einen Moment bevor du erneut postest.")
except (ValueError, TypeError):
pass
# Stunden-Limit
# Stunden-Limit (gleiche Zeitbasis wie created_at)
hour_ago = (now_dt - _td(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
if is_thread:
count = conn.execute(
"SELECT COUNT(*) FROM forum_threads WHERE user_id=? AND created_at > datetime('now','-1 hour')",
(user_id,),
"SELECT COUNT(*) FROM forum_threads WHERE user_id=? AND created_at > ?",
(user_id, hour_ago),
).fetchone()[0]
if count >= 5:
raise HTTPException(429, "Du hast in dieser Stunde bereits 5 Threads erstellt. Bitte warte etwas.")
else:
count = conn.execute(
"SELECT COUNT(*) FROM forum_posts WHERE user_id=? AND created_at > datetime('now','-1 hour')",
(user_id,),
"SELECT COUNT(*) FROM forum_posts WHERE user_id=? AND created_at > ?",
(user_id, hour_ago),
).fetchone()[0]
if count >= 20:
raise HTTPException(429, "Du hast in dieser Stunde bereits 20 Antworten geschrieben. Bitte warte etwas.")
@ -223,8 +237,8 @@ async def create_thread(data: ThreadCreate, user=Depends(get_current_user)):
if data.kategorie not in KATEGORIEN:
raise HTTPException(400, "Ungültige Kategorie.")
with db() as conn:
_check_post_limits(user["id"], conn, data.text.strip(), user.get("created_at"), is_thread=True)
ct = safe_client_time(data.client_time)
_check_post_limits(user["id"], conn, data.text.strip(), user.get("created_at"), is_thread=True, now_client=ct)
cur = conn.execute(
"""INSERT INTO forum_threads (user_id, kategorie, titel, text, thread_lat, thread_lon, thread_ort, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
@ -370,9 +384,9 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current
if thread['is_deleted']:
raise HTTPException(404, "Thread nicht gefunden.")
_check_post_limits(user["id"], conn, data.text.strip(), user.get("created_at"), is_thread=False)
ct = safe_client_time(data.client_time)
_check_post_limits(user["id"], conn, data.text.strip(), user.get("created_at"), is_thread=False, now_client=ct)
cur = conn.execute(
"INSERT INTO forum_posts (thread_id, user_id, text, created_at) VALUES (?, ?, ?, ?)",
(thread_id, user['id'], data.text.strip(), ct)