Security + E-Mail-HTML + Quartalsbericht + Registrierungspflicht
Registrierung & Login: - E-Mail-Verifikation jetzt Pflicht vor erstem Login - Register gibt keinen Token mehr zurück → "Postfach prüfen"-Screen - Login blockt mit EMAIL_NOT_VERIFIED (403) wenn unverifiziert - Resend-Verification ohne Auth (email-basiert) - Frontend: _renderVerifyPending() nach Register und Login-Fehler - Account-Lockout: 5 Fehlversuche → 15 Min gesperrt (ratelimit.py) - Login Rate-Limit zusätzlich per E-Mail-Adresse (5/5 Min) - Fehler-Tracking wird bei erfolgreichem Login zurückgesetzt E-Mail-Templates (alle Mails jetzt HTML): - email_html() Shared-Template in mailer.py (Gradient-Header, Warm-Beige) - Verifikations-Mail, Passwort-Reset → HTML mit CTA-Button - Admin-Outreach: plain text auto-wrapped in HTML - Züchter-Mails (Antrag/Genehmigung/Ablehnung) → Template - Tierschutz-Alert (litters.py) → Template - send_support_mail → HTML - outreach._build_message() + _send_smtp() unterstützen jetzt html= Parameter Forum-Schutz: - Post-Cooldown: 30 Sek zwischen beliebigen Posts (DB-Check) - Stunden-Limit: 5 Threads / 20 Antworten pro User/Stunde - Duplikat-Erkennung: gleicher Text in 5 Min blockiert (in-memory) - content_filter.py: Spam-Keywords, URL-Sperre für Accounts < 7 Tage, Sonderzeichen-Ratio-Check Security-Headers: - HSTS: max-age=31536000; includeSubDomains - Content-Security-Policy: frame-ancestors none, base-uri self, … - X-Frame-Options entfernt (CSP frame-ancestors ist moderner) Honeypot-Fallen (13 Scanner-Pfade → 24h IP-Sperre): - /api/admin/users, /api/v1/users, /api/.env, /api/config, /api/setup, /api/install, /api/phpinfo, /api/debug, /api/actuator, /api/swagger, /api/graphql u.a. Quartalsbericht-System: - backend/scripts/generate_reports.py: 6 Sections (Sicherheit, Funktionsumfang, Dateien, Nutzer, Partner, Server) - make reports: generiert alle Berichte aus dem Container, committed - Scheduler: quarterly_report Job (1. Feb/Mai/Aug/Nov 07:00) → vollständige HTML-Mail an ADMIN_EMAIL - quarterly_report erscheint im täglichen Status-Report Admin-Panel: - "Forum & Meldungen" → "Forum"
This commit is contained in:
parent
c1bb728153
commit
de1677154f
15 changed files with 1363 additions and 141 deletions
|
|
@ -7,6 +7,8 @@ from typing import Optional
|
|||
from database import db
|
||||
from auth import get_current_user, get_current_user_optional
|
||||
from timeutils import safe_client_time
|
||||
from ratelimit import is_duplicate_post, record_post
|
||||
from content_filter import check_forum_content
|
||||
from routes.push import send_push_to_user
|
||||
from media_utils import convert_media, extract_video_thumb, generate_preview, preview_url_from
|
||||
|
||||
|
|
@ -164,6 +166,50 @@ 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."""
|
||||
# 30-Sekunden-Cooldown zwischen beliebigen Posts
|
||||
last = conn.execute(
|
||||
"""SELECT MAX(created_at) AS last FROM (
|
||||
SELECT created_at FROM forum_threads WHERE user_id=?
|
||||
UNION ALL
|
||||
SELECT created_at FROM forum_posts WHERE user_id=?
|
||||
)""",
|
||||
(user_id, user_id),
|
||||
).fetchone()["last"]
|
||||
if last:
|
||||
try:
|
||||
from datetime import datetime as _dt
|
||||
diff = (_dt.utcnow() - _dt.fromisoformat(last)).total_seconds()
|
||||
if diff < 30:
|
||||
raise HTTPException(429, "Bitte warte einen Moment bevor du erneut postest.")
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# Stunden-Limit
|
||||
if is_thread:
|
||||
count = conn.execute(
|
||||
"SELECT COUNT(*) FROM forum_threads WHERE user_id=? AND created_at > datetime('now','-1 hour')",
|
||||
(user_id,),
|
||||
).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,),
|
||||
).fetchone()[0]
|
||||
if count >= 20:
|
||||
raise HTTPException(429, "Du hast in dieser Stunde bereits 20 Antworten geschrieben. Bitte warte etwas.")
|
||||
|
||||
# Duplikat-Check
|
||||
if is_duplicate_post(user_id, text):
|
||||
raise HTTPException(400, "Dieser Beitrag wurde bereits kürzlich gepostet.")
|
||||
|
||||
# Content-Filter
|
||||
check_forum_content(text, user_created_at)
|
||||
|
||||
|
||||
@router.post("/threads", status_code=201)
|
||||
async def create_thread(data: ThreadCreate, user=Depends(get_current_user)):
|
||||
if not user.get("email_verified"):
|
||||
|
|
@ -177,6 +223,7 @@ 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)
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO forum_threads (user_id, kategorie, titel, text, thread_lat, thread_lon, thread_ort, created_at)
|
||||
|
|
@ -194,6 +241,7 @@ async def create_thread(data: ThreadCreate, user=Depends(get_current_user)):
|
|||
t = dict(row)
|
||||
t['foto_urls'] = _parse_foto_urls(t.get('foto_urls'))
|
||||
t['user_liked'] = False
|
||||
record_post(user["id"], data.text.strip())
|
||||
return t
|
||||
|
||||
|
||||
|
|
@ -322,6 +370,8 @@ 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)
|
||||
cur = conn.execute(
|
||||
"INSERT INTO forum_posts (thread_id, user_id, text, created_at) VALUES (?, ?, ?, ?)",
|
||||
|
|
@ -347,6 +397,7 @@ async def create_post(thread_id: int, data: PostCreate, user=Depends(get_current
|
|||
pd = dict(row)
|
||||
pd['foto_urls'] = []
|
||||
pd['user_liked'] = False
|
||||
record_post(user["id"], data.text.strip())
|
||||
|
||||
# Push-Notification an Thread-Owner (nicht an sich selbst)
|
||||
if owner_id and owner_id != user['id']:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue