Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
716 changes: 715 additions & 1 deletion package-lock.json

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,15 @@
"react-resizable-panels": "4.7.2",
"react-router-dom": "7.13.1",
"recharts": "^3.8.1",
"rehype-autolink-headings": "^7.1.0",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1",
"rehype-raw": "^7.0.0",
"rehype-slug": "^6.0.0",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
"remark-toc": "^9.0.0",
"shiki": "^4.0.2",
"sonner": "2.0.7",
"tailwind-merge": "3.5.0",
"tailwindcss-animate": "1.0.7",
Expand Down
109 changes: 109 additions & 0 deletions src/api/dashboard/admin_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from fastapi import APIRouter, Request, HTTPException, Depends
from src.api.dashboard.auth_routes import get_current_user
from .cms.utils import is_admin
from src.bot.core.config import BotConfig
import discord

router = APIRouter(
prefix="/admin",
tags=["admin"]
)

# Shared bot instance access (imported from .routes)
def get_bot():
from .routes import bot_instance
return bot_instance

@router.get("/global-stats")
async def get_admin_global_stats(user: dict = Depends(get_current_user)):
"""Fetches global bot stats and CMS stats for the admin dashboard."""
bot = get_bot()
if bot is None:
raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar")

# Simple check for admin
is_bot_admin = user.get("id") == "cms_admin"
if not is_bot_admin:
try:
owners = getattr(BotConfig.security, 'bot_owners', [])
if int(user.get("id", 0)) in owners:
is_bot_admin = True
except: pass

Check notice on line 31 in src/api/dashboard/admin_routes.py

View check run for this annotation

codefactor.io / CodeFactor

src/api/dashboard/admin_routes.py#L31

Do not use bare 'except'. (E722)

Check notice on line 31 in src/api/dashboard/admin_routes.py

View check run for this annotation

codefactor.io / CodeFactor

src/api/dashboard/admin_routes.py#L31

Try, Except, Pass detected. (B110)

if not is_bot_admin:
raise HTTPException(status_code=403, detail="Not authorized")

try:
from mxmariadb import CMSDatabase
db = CMSDatabase()
await db.ensure_connection()
posts = await db.get_posts(published_only=False)

return {
"success": True,
"data": {
"totalGuilds": len(bot.guilds),
"totalUsers": len(bot.users),
"totalPosts": len(posts),
"apiLatency": f"{round(bot.latency * 1000)}ms",
"uptime": str(discord.utils.utcnow() - getattr(bot, 'start_time', discord.utils.utcnow())).split('.')[0]
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@router.get("/blacklist")
async def get_admin_blacklist(user: dict = Depends(get_current_user)):
from mxmariadb import BlacklistDatabase
db = BlacklistDatabase()
await db.ensure_connection()
data = await db.get_all_blacklisted()
return {"success": True, "data": data}

@router.post("/blacklist")
async def add_admin_blacklist(request: Request, user: dict = Depends(get_current_user)):
data = await request.json()
target_id = data.get("user_id")
reason = data.get("reason", "Kein Grund angegeben")
if not target_id:
raise HTTPException(status_code=400, detail="Target User ID is required")

from mxmariadb import BlacklistDatabase
db = BlacklistDatabase()
await db.ensure_connection()
success = await db.add_to_blacklist(target_id, reason, user["id"], user.get("username", "Admin"))
return {"success": success}

@router.delete("/blacklist/{target_id}")
async def remove_admin_blacklist(target_id: str, user: dict = Depends(get_current_user)):
from mxmariadb import BlacklistDatabase
db = BlacklistDatabase()
await db.ensure_connection()
success = await db.remove_from_blacklist(target_id)
return {"success": True}

@router.get("/global-chat/logs")
async def get_global_chat_logs(user: dict = Depends(get_current_user)):
from mxmariadb import GlobalChatDatabase
db = GlobalChatDatabase()
await db.ensure_connection()
query = "SELECT * FROM message_log ORDER BY timestamp DESC LIMIT 50"
data = await db.fetch_all(query)
return {"success": True, "data": data}

@router.get("/global-chat/blacklist")
async def get_global_chat_blacklist(user: dict = Depends(get_current_user)):
from mxmariadb import GlobalChatDatabase
db = GlobalChatDatabase()
await db.ensure_connection()
query = "SELECT * FROM globalchat_blacklist ORDER BY banned_at DESC"
data = await db.fetch_all(query)
return {"success": True, "data": data}

@router.get("/top-commands")
async def get_admin_top_commands(user: dict = Depends(get_current_user)):
from mxmariadb import StatsDB
db = StatsDB()
await db.ensure_connection()
data = await db.get_top_commands(limit=5)
return {"success": True, "data": data}
19 changes: 19 additions & 0 deletions src/api/dashboard/cms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from fastapi import APIRouter
from .posts import router as posts_router
from .media import router as media_router
from .tags import router as tags_router
from .roadmap import router as roadmap_router
from .team import router as team_router
from .feedback import router as feedback_router

router = APIRouter(
prefix="/cms",
tags=["cms"]
)

router.include_router(posts_router)
router.include_router(media_router)
router.include_router(tags_router)
router.include_router(roadmap_router)
router.include_router(team_router)
router.include_router(feedback_router)
74 changes: 74 additions & 0 deletions src/api/dashboard/cms/feedback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from fastapi import APIRouter, Request, HTTPException, Depends
from mxmariadb import CMSDatabase
from .utils import get_cms_db, get_maybe_user, is_admin

router = APIRouter()

@router.get("/feedback")
async def get_all_feedback(request: Request, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: Get all feedback items."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")

items = await db.get_all_feedback()
return {"success": True, "data": items}

@router.put("/feedback/{feedback_id}/status")
async def update_feedback_status(feedback_id: int, request: Request, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: Update feedback status."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")

data = await request.json()
status = data.get("status")
if status not in ["new", "read", "accepted", "rejected"]:
raise HTTPException(status_code=400, detail="Invalid status")

success = await db.update_feedback_status(feedback_id, status)
if not success:
raise HTTPException(status_code=500, detail="Failed to update status")
return {"success": True}

@router.delete("/feedback/{feedback_id}")
async def delete_feedback(feedback_id: int, request: Request, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: Delete a feedback item."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")

success = await db.delete_feedback(feedback_id)
if not success:
raise HTTPException(status_code=500, detail="Failed to delete feedback")
return {"success": True}

@router.post("/feedback/{feedback_id}/to-roadmap")
async def move_feedback_to_roadmap(feedback_id: int, request: Request, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: Move a feedback item to the roadmap and mark as accepted."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")

feedbacks = await db.get_all_feedback()
item = next((f for f in feedbacks if f["id"] == feedback_id), None)

if not item:
raise HTTPException(status_code=404, detail="Feedback not found")

if item["status"] == "accepted":
raise HTTPException(status_code=400, detail="Already moved to roadmap")

title = f"User Vorschlag ({item['user_name']})" if item["type"] == "suggestion" else f"Bugfix ({item['user_name']})"
icon = "Sparkles" if item["type"] == "suggestion" else "ShieldAlert"
description = item["content"]

success_roadmap = await db.create_roadmap_item(
title=title,
status="planned",
description=description,
icon=icon,
date_info="Demnächst"
)

if not success_roadmap:
raise HTTPException(status_code=500, detail="Failed to create roadmap item")

await db.update_feedback_status(feedback_id, "accepted")
return {"success": True}
148 changes: 148 additions & 0 deletions src/api/dashboard/cms/media.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from fastapi import APIRouter, Request, HTTPException, Depends, UploadFile, File
from fastapi.responses import HTMLResponse
import uuid
import aiofiles
from pathlib import Path
from mxmariadb import CMSDatabase
from .utils import get_cms_db, get_maybe_user, is_admin, get_requester_info, UPLOAD_DIR, ALLOWED_MIME_TYPES, MAX_FILE_SIZE

router = APIRouter()

@router.post("/upload")
async def upload_media(
request: Request,
file: UploadFile = File(...),
is_stock: bool = False,
user: dict = Depends(get_maybe_user),
db: CMSDatabase = Depends(get_cms_db)
):
"""Admin: upload a media file. Returns the public URL."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")

if file.content_type not in ALLOWED_MIME_TYPES:
raise HTTPException(status_code=415, detail=f"Unsupported file type: {file.content_type}")

content = await file.read()
if len(content) > MAX_FILE_SIZE:
raise HTTPException(status_code=413, detail="File too large (max 10 MB)")

ext = Path(file.filename).suffix.lower() if file.filename else ""
unique_name = f"{uuid.uuid4().hex}{ext}"
file_path = UPLOAD_DIR / unique_name

async with aiofiles.open(file_path, "wb") as f:
await f.write(content)

user_id, username = get_requester_info(request, user)
form_data = await request.form()
stock_flag = form_data.get("is_stock") == "true" or is_stock

await db.create_media(
filename=unique_name,
original_name=file.filename or unique_name,
mime_type=file.content_type,
size_bytes=len(content),
uploader_id=user_id,
uploader_name=username,
is_stock=stock_flag
)

public_url = f"/uploads/cms/{unique_name}"
return {"success": True, "url": public_url, "filename": unique_name, "is_stock": stock_flag}

@router.get("/media")
async def list_media(request: Request, is_stock: bool = None, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: list uploaded media files, optionally filtered by stock status."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")
try:
media = await db.get_media(is_stock=is_stock)
for m in media:
m["url"] = f"/uploads/cms/{m['filename']}"
return {"success": True, "data": media}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

@router.put("/media/{media_id}")
async def update_media_stock(media_id: int, request: Request, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: toggle is_stock flag for media."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")

data = await request.json()
success = await db.update_media(media_id, data.get("is_stock", False))
if not success:
raise HTTPException(status_code=500, detail="Failed to update media")
return {"success": True}

@router.get("/media/view/{media_id}", response_class=HTMLResponse)
async def view_media_embed(media_id: int, request: Request, db: CMSDatabase = Depends(get_cms_db)):
"""Public: Returns an HTML page with Open Graph tags for Discord embeds."""
try:
media_list = await db.get_media(limit=1000)
media_item = next((m for m in media_list if m["id"] == media_id), None)

if not media_item:
return HTMLResponse(content="<h1>Media not found</h1>", status_code=404)

base_url = str(request.base_url).rstrip('/')
image_url = f"{base_url}/uploads/cms/{media_item['filename']}"

date_str = "Unknown date"
if media_item.get("uploaded_at"):
date_str = media_item["uploaded_at"].strftime("%d.%m.%Y %H:%M")

title = media_item["original_name"]
description = f"Hochgeladen am: {date_str} von {media_item.get('uploader_name', 'Unknown')}"

html_content = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title} - ManagerX Media</title>
<meta property="og:type" content="website">
<meta property="og:title" content="{title}">
<meta property="og:description" content="{description}">
<meta property="og:image" content="{image_url}">
<meta name="theme-color" content="#3498db">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{title}">
<meta name="twitter:description" content="{description}">
<meta name="twitter:image" content="{image_url}">
<style>
body {{ background-color: #050505; color: white; font-family: sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; }}
img {{ max-width: 90%; max-height: 80vh; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }}
.info {{ margin-top: 20px; text-align: center; color: #a1a1aa; }}
h1 {{ font-size: 1.2rem; color: #fff; margin-bottom: 5px; }}
</style>
</head>
<body>
<img src="{image_url}" alt="{title}">
<div class="info">
<h1>{title}</h1>
<p>{description}</p>
</div>
</body>
</html>
"""
return HTMLResponse(content=html_content)
except Exception as e:
return HTMLResponse(content=f"<h1>Error</h1><p>{str(e)}</p>", status_code=500)

@router.delete("/media/{media_id}")
async def delete_media(media_id: int, request: Request, user: dict = Depends(get_maybe_user), db: CMSDatabase = Depends(get_cms_db)):
"""Admin: delete a media file from DB and disk."""
if not is_admin(request, user):
raise HTTPException(status_code=403, detail="Not authorized")
try:
filename = await db.delete_media(media_id)
if filename:
file_path = UPLOAD_DIR / filename
if file_path.exists():
file_path.unlink()
return {"success": True}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
Loading