banyaro/backend/routes/moderation.py

285 lines
11 KiB
Python

"""BAN YARO — Moderations-Panel Backend"""
from datetime import datetime
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
pending_poi_edits = 0
try:
pending_poi_edits = conn.execute(
"SELECT COUNT(*) FROM osm_poi_edits WHERE status='pending'"
).fetchone()[0]
except Exception:
pass
return {
"open_reports": open_reports,
"pending_fotos": pending_fotos,
"banned_users": banned_users,
"pending_zuchter": pending_zuchter,
"pending_poi_edits": pending_poi_edits,
}
# ------------------------------------------------------------------
# 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, r.resolved_at,
u.name AS melder_name,
m.name AS resolved_by_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
LEFT JOIN users m ON m.id=r.resolved_by
ORDER BY r.resolved ASC, r.created_at DESC
LIMIT 200
""").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=?, resolved_by=?, resolved_at=?
WHERE id=?""",
(new_state,
user["id"] if new_state else None,
datetime.utcnow().isoformat() if new_state else None,
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),
):
is_admin = user["rolle"] == "admin"
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"
# Moderatoren sehen keine Admins
if not is_admin:
where += " AND rolle != 'admin' AND COALESCE(is_admin, 0) = 0"
# E-Mail nur für Admins; Moderatoren sehen maskierte Version
email_col = "email" if is_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, is_admin, name FROM users WHERE id=?", (uid,)
).fetchone()
if not target:
raise HTTPException(404, "User nicht gefunden.")
# Moderatoren dürfen keine Admins bearbeiten
if user["rolle"] != "admin" and (
target["rolle"] == "admin" or target["is_admin"]
):
raise HTTPException(403, "Admins können nicht von Moderatoren bearbeitet 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:
rows = conn.execute("""
SELECT s.id, s.foto_url, s.status, s.created_at,
s.reviewed_at, s.reject_reason,
COALESCE(s.rights_confirmed, 0) AS rights_confirmed,
u.name AS user_name,
m.name AS reviewed_by_name,
r.name AS rasse_name, r.slug AS rasse_slug,
r.foto_url AS aktuell_foto
FROM wiki_foto_submissions s
LEFT JOIN users u ON u.id = s.user_id
LEFT JOIN users m ON m.id = s.reviewed_by
LEFT JOIN wiki_rassen r ON r.id = s.rasse_id
ORDER BY s.status ASC, s.created_at ASC
LIMIT 200
""").fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# 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)):
"""Delegiert an die wiki-Route — dort ist die vollständige Logik (inkl. Datei-Kopie)."""
from routes.wiki import review_submission, ReviewModel
model = ReviewModel(
action=data.get("action", ""),
reject_reason=data.get("reject_reason", ""),
)
return await review_submission(foto_id, model, user)
# ------------------------------------------------------------------
# GET /api/moderation/poi-edits — ausstehende POI-Korrekturen
# ------------------------------------------------------------------
@router.get("/poi-edits")
async def mod_poi_edits(user=Depends(require_moderator)):
with db() as conn:
rows = conn.execute("""
SELECT e.id, e.osm_id, e.poi_name, e.field,
e.old_value, e.new_value, e.status,
e.created_at, e.resolved_at,
u.name AS einreicher_name,
m.name AS mod_name
FROM osm_poi_edits e
JOIN users u ON u.id = e.user_id
LEFT JOIN users m ON m.id = e.mod_id
ORDER BY e.status ASC, e.created_at DESC
LIMIT 200
""").fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# PATCH /api/moderation/poi-edits/{id} — approve / reject
# ------------------------------------------------------------------
@router.patch("/poi-edits/{edit_id}")
async def mod_poi_edit_action(edit_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:
edit = conn.execute(
"SELECT * FROM osm_poi_edits WHERE id=?", (edit_id,)
).fetchone()
if not edit:
raise HTTPException(404, "Korrektur nicht gefunden.")
if edit["status"] != "pending":
raise HTTPException(409, "Korrektur wurde bereits bearbeitet.")
if action == "approve":
_ALLOWED_POI_FIELDS = {"opening_hours", "phone", "website", "name"}
if edit["field"] not in _ALLOWED_POI_FIELDS:
raise HTTPException(400, f"Ungültiges Feld: {edit['field']}")
conn.execute(
f"UPDATE osm_pois SET {edit['field']}=?, user_edited=1 WHERE osm_id=?",
(edit["new_value"], edit["osm_id"])
)
conn.execute(
"""UPDATE osm_poi_edits SET status=?, mod_id=?, resolved_at=datetime('now')
WHERE id=?""",
(action + "d", user["id"], edit_id)
)
return {"status": action + "d"}