From 8ba8f4dfa36b29f066111bbae9ef7c319892ad34 Mon Sep 17 00:00:00 2001 From: rene Date: Sat, 25 Apr 2026 08:19:19 +0200 Subject: [PATCH] =?UTF-8?q?Moderations-Panel:=20neue=20Seite=20/moderation?= =?UTF-8?q?=20f=C3=BCr=20Mods=20und=20Admins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: routes/moderation.py mit GET /stats, /reports, /users, /fotos und PATCH-Endpoints für Ban/Unban und Foto-Review - Frontend: pages/moderation.js mit 4 Tabs (Übersicht, Fotos, User, Forum) - Sidebar-Eintrag (nur für Moderatoren/Admins sichtbar, gelb) - Page in index.html registriert, pages-Objekt in app.js ergänzt - Router in main.py eingebunden (/api/moderation) - SW-Cache by-v357, app.js/ui.js/api.js auf v=94 --- backend/main.py | 2 + backend/routes/moderation.py | 223 +++++++++++++ backend/static/index.html | 15 +- backend/static/js/app.js | 7 + backend/static/js/pages/moderation.js | 447 ++++++++++++++++++++++++++ backend/static/sw.js | 2 +- 6 files changed, 692 insertions(+), 4 deletions(-) create mode 100644 backend/routes/moderation.py create mode 100644 backend/static/js/pages/moderation.js diff --git a/backend/main.py b/backend/main.py index 2865269..4cfad18 100644 --- a/backend/main.py +++ b/backend/main.py @@ -121,6 +121,7 @@ from routes.training import router as training_router from routes.praise import router as praise_router from routes.weather import router as weather_router from routes.social import router as social_router +from routes.moderation import router as moderation_router app.include_router(auth_router, prefix="/api/auth", tags=["Auth"]) app.include_router(dogs_router, prefix="/api/dogs", tags=["Hunde"]) @@ -161,6 +162,7 @@ app.include_router(stats_router, prefix="/api/stats", tags=[ app.include_router(achievements_router, prefix="/api/achievements", tags=["Achievements"]) app.include_router(training_router, prefix="/api/training", tags=["Training"]) app.include_router(praise_router, prefix="/api/praise", tags=["Praise"]) +app.include_router(moderation_router, prefix="/api/moderation", tags=["Moderation"]) # ------------------------------------------------------------------ diff --git a/backend/routes/moderation.py b/backend/routes/moderation.py new file mode 100644 index 0000000..128748b --- /dev/null +++ b/backend/routes/moderation.py @@ -0,0 +1,223 @@ +"""BAN YARO — Moderations-Panel Backend""" +from fastapi import APIRouter, Depends, HTTPException +from database import db +from auth import get_current_user + +router = APIRouter() + + +# ------------------------------------------------------------------ +# Dependency: Moderator oder Admin +# ------------------------------------------------------------------ +def require_moderator(user=Depends(get_current_user)): + if not (user.get("is_moderator") or user["rolle"] == "admin"): + raise HTTPException(403, "Nur für Moderatoren.") + return user + + +# ------------------------------------------------------------------ +# GET /api/moderation/stats — Übersicht +# ------------------------------------------------------------------ +@router.get("/stats") +async def mod_stats(user=Depends(require_moderator)): + with db() as conn: + open_reports = conn.execute( + "SELECT COUNT(*) FROM forum_reports WHERE resolved=0" + ).fetchone()[0] + + pending_fotos = 0 + try: + pending_fotos = conn.execute( + "SELECT COUNT(*) FROM wiki_foto_submissions WHERE status='pending'" + ).fetchone()[0] + except Exception: + pass + + banned_users = conn.execute( + "SELECT COUNT(*) FROM users WHERE is_banned=1" + ).fetchone()[0] + + pending_zuchter = 0 + try: + pending_zuchter = conn.execute( + "SELECT COUNT(*) FROM wiki_zuchter WHERE verified=0" + ).fetchone()[0] + except Exception: + pass + + return { + "open_reports": open_reports, + "pending_fotos": pending_fotos, + "banned_users": banned_users, + "pending_zuchter": pending_zuchter, + } + + +# ------------------------------------------------------------------ +# GET /api/moderation/reports — gemeldete Inhalte +# ------------------------------------------------------------------ +@router.get("/reports") +async def mod_reports(user=Depends(require_moderator)): + with db() as conn: + rows = conn.execute(""" + SELECT r.id, r.target_type, r.target_id, r.grund, r.resolved, r.created_at, + u.name AS melder_name, + CASE r.target_type + WHEN 'thread' THEN (SELECT t.titel FROM forum_threads t WHERE t.id=r.target_id) + WHEN 'post' THEN (SELECT SUBSTR(p.text,1,80) FROM forum_posts p WHERE p.id=r.target_id) + END AS content_preview + FROM forum_reports r + LEFT JOIN users u ON u.id=r.user_id + WHERE r.resolved=0 + ORDER BY r.created_at DESC + LIMIT 100 + """).fetchall() + return [dict(r) for r in rows] + + +# ------------------------------------------------------------------ +# PATCH /api/moderation/reports/{id} — Meldung erledigen +# ------------------------------------------------------------------ +@router.patch("/reports/{rid}") +async def mod_resolve_report(rid: int, user=Depends(require_moderator)): + with db() as conn: + r = conn.execute( + "SELECT resolved FROM forum_reports WHERE id=?", (rid,) + ).fetchone() + if not r: + raise HTTPException(404, "Meldung nicht gefunden.") + new_state = 0 if r["resolved"] else 1 + conn.execute( + "UPDATE forum_reports SET resolved=? WHERE id=?", + (new_state, rid) + ) + return {"ok": True} + + +# ------------------------------------------------------------------ +# GET /api/moderation/users — User-Liste (Basisinfos) +# ------------------------------------------------------------------ +@router.get("/users") +async def mod_users( + q: str = "", + banned: int = 0, + limit: int = 50, + offset: int = 0, + user=Depends(require_moderator), +): + with db() as conn: + where = "WHERE 1=1" + params = [] + if q.strip(): + where += " AND (name LIKE ? OR email LIKE ?)" + params.extend([f"%{q.strip()}%", f"%{q.strip()}%"]) + if banned: + where += " AND is_banned=1" + + # E-Mail nur für Admins; Moderatoren sehen maskierte Version + email_col = "email" if user["rolle"] == "admin" else \ + "SUBSTR(email,1,2)||'***@'||SUBSTR(email,INSTR(email,'@')+1) AS email" + + rows = conn.execute(f""" + SELECT id, name, {email_col}, rolle, is_moderator, is_banned, ban_reason, created_at + FROM users + {where} + ORDER BY created_at DESC + LIMIT ? OFFSET ? + """, [*params, limit, offset]).fetchall() + + total = conn.execute( + f"SELECT COUNT(*) FROM users {where}", params + ).fetchone()[0] + + return {"users": [dict(r) for r in rows], "total": total} + + +# ------------------------------------------------------------------ +# PATCH /api/moderation/users/{id} — Ban / Unban +# ------------------------------------------------------------------ +@router.patch("/users/{uid}") +async def mod_patch_user(uid: int, data: dict, user=Depends(require_moderator)): + allowed_fields = {"is_banned", "ban_reason"} + updates = {k: v for k, v in data.items() if k in allowed_fields} + if not updates: + raise HTTPException(400, "Keine erlaubten Felder.") + + with db() as conn: + target = conn.execute( + "SELECT id, rolle, name FROM users WHERE id=?", (uid,) + ).fetchone() + if not target: + raise HTTPException(404, "User nicht gefunden.") + if target["rolle"] == "admin" and user["rolle"] != "admin": + raise HTTPException(403, "Admins können nur von Admins verwaltet werden.") + + cols = ", ".join(f"{k}=?" for k in updates) + conn.execute( + f"UPDATE users SET {cols} WHERE id=?", + [*updates.values(), uid] + ) + row = conn.execute( + "SELECT id, name, rolle, is_banned, ban_reason FROM users WHERE id=?", + (uid,) + ).fetchone() + return dict(row) + + +# ------------------------------------------------------------------ +# GET /api/moderation/fotos — Wiki-Foto-Einreichungen (pending) +# ------------------------------------------------------------------ +@router.get("/fotos") +async def mod_fotos(user=Depends(require_moderator)): + with db() as conn: + try: + rows = conn.execute(""" + SELECT s.id, s.rasse_slug, s.foto_url, s.created_at, + u.name AS user_name, + r.name AS rasse_name, r.foto_url AS aktuell_foto + FROM wiki_foto_submissions s + LEFT JOIN users u ON u.id=s.user_id + LEFT JOIN wiki_rassen r ON r.slug=s.rasse_slug + WHERE s.status='pending' + ORDER BY s.created_at ASC + LIMIT 50 + """).fetchall() + return [dict(r) for r in rows] + except Exception: + return [] + + +# ------------------------------------------------------------------ +# PATCH /api/moderation/fotos/{id} — Foto genehmigen / ablehnen +# ------------------------------------------------------------------ +@router.patch("/fotos/{foto_id}") +async def mod_foto_action(foto_id: int, data: dict, user=Depends(require_moderator)): + action = data.get("action") + if action not in ("approve", "reject"): + raise HTTPException(400, "action muss 'approve' oder 'reject' sein.") + + with db() as conn: + sub = conn.execute( + "SELECT id, rasse_slug, foto_url FROM wiki_foto_submissions WHERE id=?", + (foto_id,) + ).fetchone() + if not sub: + raise HTTPException(404, "Einreichung nicht gefunden.") + + if action == "approve": + conn.execute( + "UPDATE wiki_foto_submissions SET status='approved' WHERE id=?", + (foto_id,) + ) + conn.execute( + "UPDATE wiki_rassen SET foto_url=? WHERE slug=?", + (sub["foto_url"], sub["rasse_slug"]) + ) + else: + reason = data.get("reject_reason", "Nicht geeignet.") + conn.execute( + "UPDATE wiki_foto_submissions SET status='rejected', reject_reason=? WHERE id=?", + (reason, foto_id) + ) + + return {"ok": True} diff --git a/backend/static/index.html b/backend/static/index.html index ba0cd02..14b694d 100644 --- a/backend/static/index.html +++ b/backend/static/index.html @@ -185,6 +185,11 @@ Social Media + +