banyaro/backend/routes/moderation.py
rene 8ba8f4dfa3 Moderations-Panel: neue Seite /moderation für Mods und Admins
- 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
2026-04-25 08:19:19 +02:00

223 lines
8.1 KiB
Python

"""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}