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