285 lines
11 KiB
Python
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"}
|