"""BAN YARO — Admin / Moderator Backend""" from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel from typing import Optional from database import db from auth import get_current_user router = APIRouter() # ------------------------------------------------------------------ # Dependency: Moderator oder Admin # ------------------------------------------------------------------ def require_mod(user=Depends(get_current_user)): if user["rolle"] not in ("admin", "moderator") and not user.get("is_moderator"): raise HTTPException(403, "Kein Zugriff.") return user def require_admin(user=Depends(get_current_user)): if user["rolle"] != "admin": raise HTTPException(403, "Nur Admins.") return user # ------------------------------------------------------------------ # Schemas # ------------------------------------------------------------------ class UserPatch(BaseModel): rolle: Optional[str] = None # user | moderator | admin is_moderator: Optional[int] = None is_banned: Optional[int] = None ban_reason: Optional[str] = None class ThreadAdminPatch(BaseModel): is_pinned: Optional[int] = None is_locked: Optional[int] = None is_deleted: Optional[int] = None # ------------------------------------------------------------------ # GET /api/admin/stats # ------------------------------------------------------------------ @router.get("/stats") async def stats(user=Depends(require_mod)): with db() as conn: users_total = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0] users_today = conn.execute( "SELECT COUNT(*) FROM users WHERE DATE(created_at)=DATE('now')" ).fetchone()[0] threads = conn.execute( "SELECT COUNT(*) FROM forum_threads WHERE is_deleted=0" ).fetchone()[0] posts = conn.execute( "SELECT COUNT(*) FROM forum_posts WHERE is_deleted=0" ).fetchone()[0] open_reports = conn.execute( "SELECT COUNT(*) FROM forum_reports WHERE resolved=0" ).fetchone()[0] banned = conn.execute( "SELECT COUNT(*) FROM users WHERE is_banned=1" ).fetchone()[0] dogs_total = conn.execute("SELECT COUNT(*) FROM dogs").fetchone()[0] poison_total = conn.execute("SELECT COUNT(*) FROM poison WHERE geloest=0 AND expires_at > datetime('now')").fetchone()[0] return { "users_total": users_total, "users_today": users_today, "threads": threads, "posts": posts, "open_reports": open_reports, "banned": banned, "dogs_total": dogs_total, "poison_active":poison_total, } # ------------------------------------------------------------------ # GET /api/admin/users # ------------------------------------------------------------------ @router.get("/users") async def list_users( q: str = "", rolle: str = "", limit: int = 50, offset: int = 0, user=Depends(require_mod), ): with db() as conn: where = "WHERE 1=1" params = [] if q.strip(): where += " AND (u.name LIKE ? OR u.email LIKE ?)" params.extend([f"%{q.strip()}%", f"%{q.strip()}%"]) if rolle: where += " AND u.rolle = ?" params.append(rolle) rows = conn.execute(f""" SELECT u.id, u.name, u.email, u.rolle, u.is_premium, u.is_moderator, u.is_banned, u.ban_reason, u.created_at, u.last_login, (SELECT COUNT(*) FROM dogs d WHERE d.user_id=u.id) AS dog_count, (SELECT COUNT(*) FROM forum_threads t WHERE t.user_id=u.id AND t.is_deleted=0) AS thread_count FROM users u {where} ORDER BY u.created_at DESC LIMIT ? OFFSET ? """, [*params, limit, offset]).fetchall() total = conn.execute(f""" SELECT COUNT(*) FROM users u {where} """, params).fetchone()[0] return {"users": [dict(r) for r in rows], "total": total} # ------------------------------------------------------------------ # PATCH /api/admin/users/{id} — Rolle, Sperre # ------------------------------------------------------------------ @router.patch("/users/{uid}") async def patch_user(uid: int, data: UserPatch, user=Depends(require_mod)): # Rollenwechsel nur für Admins if data.rolle is not None and user["rolle"] != "admin": raise HTTPException(403, "Rollenwechsel nur für Admins.") if data.rolle and data.rolle not in ("user", "moderator", "admin"): raise HTTPException(400, "Ungültige Rolle.") with db() as conn: target = conn.execute("SELECT id, rolle FROM users WHERE id=?", (uid,)).fetchone() if not target: raise HTTPException(404, "User nicht gefunden.") # Mods dürfen keine Admins sperren if target["rolle"] == "admin" and user["rolle"] != "admin": raise HTTPException(403, "Admins können nur von Admins verwaltet werden.") updates = data.model_dump(exclude_none=True) if not updates: raise HTTPException(400, "Keine Änderungen.") # is_moderator aus rolle ableiten wenn rolle gesetzt wird if "rolle" in updates: updates["is_moderator"] = 1 if updates["rolle"] in ("moderator", "admin") else 0 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, email, rolle, is_moderator, is_banned, ban_reason FROM users WHERE id=?", (uid,) ).fetchone() return dict(row) # ------------------------------------------------------------------ # DELETE /api/admin/users/{id} — Account löschen (Admin only) # ------------------------------------------------------------------ @router.delete("/users/{uid}", status_code=204) async def delete_user(uid: int, user=Depends(require_admin)): with db() as conn: target = conn.execute("SELECT id, rolle FROM users WHERE id=?", (uid,)).fetchone() if not target: raise HTTPException(404, "User nicht gefunden.") if target["id"] == user["id"]: raise HTTPException(400, "Du kannst deinen eigenen Account nicht löschen.") conn.execute("DELETE FROM users WHERE id=?", (uid,)) # ------------------------------------------------------------------ # GET /api/admin/forum/threads — alle Threads inkl. gelöschte # ------------------------------------------------------------------ @router.get("/forum/threads") async def admin_threads( q: str = "", deleted: int = 0, limit: int = 50, offset: int = 0, user=Depends(require_mod), ): with db() as conn: where = "WHERE 1=1" params = [] if not deleted: where += " AND t.is_deleted=0" if q.strip(): where += " AND (t.titel LIKE ? OR t.text LIKE ?)" params.extend([f"%{q.strip()}%", f"%{q.strip()}%"]) rows = conn.execute(f""" SELECT t.id, t.kategorie, t.titel, SUBSTR(t.text,1,100) AS text_preview, t.antworten, t.likes, t.views, t.is_pinned, t.is_locked, t.is_deleted, t.created_at, u.id AS user_id, u.name AS autor_name FROM forum_threads t LEFT JOIN users u ON u.id=t.user_id {where} ORDER BY t.created_at DESC LIMIT ? OFFSET ? """, [*params, limit, offset]).fetchall() total = conn.execute(f""" SELECT COUNT(*) FROM forum_threads t {where} """, params).fetchone()[0] return {"threads": [dict(r) for r in rows], "total": total} # ------------------------------------------------------------------ # PATCH /api/admin/forum/threads/{id} # ------------------------------------------------------------------ @router.patch("/forum/threads/{tid}") async def admin_patch_thread(tid: int, data: ThreadAdminPatch, user=Depends(require_mod)): with db() as conn: if not conn.execute("SELECT 1 FROM forum_threads WHERE id=?", (tid,)).fetchone(): raise HTTPException(404, "Thread nicht gefunden.") updates = data.model_dump(exclude_none=True) if not updates: raise HTTPException(400, "Keine Änderungen.") cols = ", ".join(f"{k}=?" for k in updates) conn.execute(f"UPDATE forum_threads SET {cols} WHERE id=?", [*updates.values(), tid]) return {"ok": True} # ------------------------------------------------------------------ # DELETE /api/admin/forum/threads/{id} # ------------------------------------------------------------------ @router.delete("/forum/threads/{tid}", status_code=204) async def admin_delete_thread(tid: int, user=Depends(require_mod)): with db() as conn: if not conn.execute("SELECT 1 FROM forum_threads WHERE id=?", (tid,)).fetchone(): raise HTTPException(404, "Thread nicht gefunden.") conn.execute("UPDATE forum_threads SET is_deleted=1 WHERE id=?", (tid,)) # ------------------------------------------------------------------ # DELETE /api/admin/forum/posts/{id} # ------------------------------------------------------------------ @router.delete("/forum/posts/{pid}", status_code=204) async def admin_delete_post(pid: int, user=Depends(require_mod)): with db() as conn: post = conn.execute("SELECT * FROM forum_posts WHERE id=?", (pid,)).fetchone() if not post: raise HTTPException(404, "Beitrag nicht gefunden.") conn.execute("UPDATE forum_posts SET is_deleted=1 WHERE id=?", (pid,)) conn.execute( "UPDATE forum_threads SET antworten=MAX(0,antworten-1) WHERE id=?", (post["thread_id"],) ) # ------------------------------------------------------------------ # GET /api/admin/reports — offene Meldungen # ------------------------------------------------------------------ @router.get("/reports") async def admin_reports(user=Depends(require_mod)): 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 ORDER BY r.resolved ASC, r.created_at DESC LIMIT 100 """).fetchall() return [dict(r) for r in rows] # ------------------------------------------------------------------ # PATCH /api/admin/reports/{id} — erledigen / wiedereröffnen # ------------------------------------------------------------------ @router.patch("/reports/{rid}") async def admin_resolve_report(rid: int, user=Depends(require_mod)): 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.") conn.execute( "UPDATE forum_reports SET resolved=? WHERE id=?", (0 if r["resolved"] else 1, rid) ) return {"ok": True}