banyaro/backend/routes/admin.py

284 lines
11 KiB
Python

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