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
This commit is contained in:
parent
d0abb6de9b
commit
8ba8f4dfa3
6 changed files with 692 additions and 4 deletions
223
backend/routes/moderation.py
Normal file
223
backend/routes/moderation.py
Normal file
|
|
@ -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}
|
||||
Loading…
Add table
Add a link
Reference in a new issue