diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index bee0d8b..c1867a5 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -35,7 +35,11 @@ jobs: run: npm install --legacy-peer-deps - name: Build project - run: npm run build # WICHTIG: Erstellt den fertigen 'dist' Ordner + run: npm run build + env: + VITE_API_URL: ${{ secrets.VITE_API_URL }} + VITE_DISCORD_CLIENT_ID: ${{ secrets.VITE_DISCORD_CLIENT_ID }} + VITE_DISCORD_REDIRECT_URI: ${{ secrets.VITE_DISCORD_REDIRECT_URI }} - name: Setup Pages uses: actions/configure-pages@v5 diff --git a/README.md b/README.md index 8e8e4e4..56c2f57 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ **Entwickelt von** [**ManagerX Development**](https://github.com/ManagerX-Development) **|** ⚡ **Powered by OPPRO.NET Network™** + +Webseite: https://managerx-bot.de +Dokumentation: https://docs.managerx-bot.de +API: https://api.managerx-bot.de

diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css index 4b4c030..1aea4b3 100644 --- a/docs/source/_static/custom.css +++ b/docs/source/_static/custom.css @@ -3,8 +3,6 @@ PyData Sphinx Theme - Optimized & Refined ========================================================================== */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap'); - :root { /* ManagerX Premium Color System */ --mx-red-primary: #dc2626; @@ -13,7 +11,7 @@ --mx-red-light: #fef2f2; --mx-red-accent: #f87171; --mx-red-glow: rgba(220, 38, 38, 0.1); - + /* Neutral Palette */ --mx-gray-50: #f8fafc; --mx-gray-100: #f1f5f9; @@ -25,37 +23,37 @@ --mx-gray-700: #334155; --mx-gray-800: #1e293b; --mx-gray-900: #0f172a; - + /* Semantic Colors */ --mx-success: #059669; --mx-warning: #d97706; --mx-danger: #dc2626; --mx-info: #0284c7; - + /* Typography System */ --pst-font-family-base: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; --pst-font-family-heading: 'Space Grotesk', 'Inter', sans-serif; --pst-font-family-monospace: 'JetBrains Mono', 'Consolas', 'Monaco', monospace; - + /* PyData Theme Overrides */ --pst-color-primary: var(--mx-red-primary); --pst-color-secondary: var(--mx-gray-600); --pst-color-link: var(--mx-red-primary); --pst-color-link-hover: var(--mx-red-dark); --pst-color-target: #fbbf24; - + /* Shadows & Effects */ --mx-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05); --mx-shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.06); --mx-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); --mx-shadow-lg: 0 10px 24px rgba(0, 0, 0, 0.1); --mx-shadow-xl: 0 20px 40px rgba(0, 0, 0, 0.12); - + /* Transitions */ --mx-transition-fast: 0.15s cubic-bezier(0.4, 0, 0.2, 1); --mx-transition-base: 0.25s cubic-bezier(0.4, 0, 0.2, 1); --mx-transition-slow: 0.4s cubic-bezier(0.4, 0, 0.2, 1); - + /* Border Radius */ --mx-radius-sm: 6px; --mx-radius-md: 10px; @@ -107,7 +105,12 @@ body { } /* Improved Typography Hierarchy */ -h1, h2, h3, h4, h5, h6 { +h1, +h2, +h3, +h4, +h5, +h6 { font-family: var(--pst-font-family-heading); font-weight: 700; letter-spacing: -0.025em; @@ -255,8 +258,8 @@ h2::before { } /* Active Navigation Item */ -.bd-sidebar-primary .nav-item.current > a, -.bd-sidebar-primary .nav-item.active > a { +.bd-sidebar-primary .nav-item.current>a, +.bd-sidebar-primary .nav-item.active>a { background: linear-gradient(90deg, var(--mx-red-light) 0%, transparent 100%); color: var(--mx-red-primary) !important; font-weight: 600; @@ -264,7 +267,7 @@ h2::before { padding-left: calc(1rem - 3px); } -[data-theme="dark"] .bd-sidebar-primary .nav-item.current > a { +[data-theme="dark"] .bd-sidebar-primary .nav-item.current>a { background: linear-gradient(90deg, rgba(220, 38, 38, 0.15) 0%, transparent 100%); } @@ -301,10 +304,13 @@ article p { } @keyframes highlight-pulse { - 0%, 50% { + + 0%, + 50% { background-color: var(--mx-red-glow); box-shadow: 0 0 0 8px var(--mx-red-glow); } + 100% { background-color: transparent; box-shadow: 0 0 0 0 transparent; @@ -861,11 +867,9 @@ dt.sig { .mx-hero { text-align: center; padding: 5rem 2rem; - background: radial-gradient( - circle at center, - var(--mx-red-glow) 0%, - transparent 70% - ); + background: radial-gradient(circle at center, + var(--mx-red-glow) 0%, + transparent 70%); border-radius: var(--mx-radius-xl); margin: 3rem 0; } @@ -950,19 +954,19 @@ dt.sig { h1 { font-size: 2rem; } - + h2 { font-size: 1.65rem; } - + h3 { font-size: 1.35rem; } - + .bd-main { padding-top: 1rem; } - + .mx-hero { padding: 3rem 1.5rem; } @@ -972,19 +976,19 @@ dt.sig { h1 { font-size: 1.75rem; } - + h2 { font-size: 1.5rem; } - + .admonition { padding: 1.25rem !important; } - + table.docutils { font-size: 0.85rem; } - + .prev-next-area a { padding: 1.25rem !important; } @@ -995,6 +999,7 @@ dt.sig { ========================================================================== */ @media print { + .bd-header, .bd-sidebar-primary, .bd-sidebar-secondary, @@ -1002,11 +1007,11 @@ dt.sig { .prev-next-area { display: none !important; } - + article { max-width: 100% !important; } - + .admonition { page-break-inside: avoid; } @@ -1044,6 +1049,7 @@ input:focus-visible { /* Reduced Motion */ @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { diff --git a/main.py b/main.py index dd2dc30..f45bafb 100644 --- a/main.py +++ b/main.py @@ -25,6 +25,12 @@ # Logger (muss existieren!) from logger import logger +# ============================================================================= +# SETUP +# ============================================================================= +BASEDIR = Path(__file__).resolve().parent +load_dotenv(dotenv_path=BASEDIR / 'config' / '.env') + # Lokale Module aus src/bot/core from src.bot.core.config import ConfigLoader, BotConfig from src.bot.core.bot_setup import BotSetup @@ -37,11 +43,6 @@ from src.api.dashboard.routes import set_bot_instance, router as dashboard_router from mx_handler import TranslationHandler -# ============================================================================= -# SETUP -# ============================================================================= -BASEDIR = Path(__file__).resolve().parent -load_dotenv(dotenv_path=BASEDIR / 'config' / '.env') colorama_init(autoreset=True) TranslationHandler.settings( @@ -104,7 +105,7 @@ async def start_webserver(): bot = bot_setup.create_bot() # Speichere Bot Start-Zeit für Uptime-Berechnung - bot.start_time = datetime.utcnow() + bot.start_time = discord.utils.utcnow() # Übergebe Bot-Instanz an die API-Routes set_bot_instance(bot) diff --git a/package-lock.json b/package-lock.json index f32aa49..10b41fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7463,4 +7463,4 @@ } } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index fdebf70..56be7e0 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "react-hook-form": "7.71.2", "react-resizable-panels": "4.7.2", "react-router-dom": "7.13.1", - "recharts": "3.8.0", "sonner": "2.0.7", "tailwind-merge": "3.5.0", "tailwindcss-animate": "1.0.7", @@ -88,4 +87,4 @@ "vite": "8.0.0", "vitest": "4.1.0" } -} +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 4cf3e08..2a2109e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "ManagerX" -version = "2.0.0+build29cbfd2" +version = "2.0.0+build9b58708" description = "A powerful Discord bot for server management and fun." readme = "README.md" requires-python = ">=3.8" diff --git a/requirements/req.txt b/requirements/req.txt index b1455aa..831314b 100644 --- a/requirements/req.txt +++ b/requirements/req.txt @@ -82,4 +82,4 @@ jaraco.classes==3.4.0 jaraco.context==6.1.1 jaraco.functools==4.4.0 docutils==0.22.4 -nh3==0.3.2Ne \ No newline at end of file +nh3==0.3.2 \ No newline at end of file diff --git a/src/api/dashboard/auth_routes.py b/src/api/dashboard/auth_routes.py new file mode 100644 index 0000000..e5dec6a --- /dev/null +++ b/src/api/dashboard/auth_routes.py @@ -0,0 +1,169 @@ +from fastapi import APIRouter, Request, HTTPException, Security, status, Depends +from fastapi.responses import RedirectResponse +import httpx +import jwt +import os +import time +from urllib.parse import urlencode + +router = APIRouter( + prefix="/auth", + tags=["auth"] +) + +# JWT Setup +JWT_SECRET = os.getenv("JWT_SECRET", "fallback-secret") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 7 # 7 days + +# Discord OAuth Setup +CLIENT_ID = os.getenv("DISCORD_CLIENT_ID") +CLIENT_SECRET = os.getenv("DISCORD_CLIENT_SECRET") +REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI", "http://localhost:8080/dash/auth/callback") +DASHBOARD_URL = os.getenv("DASHBOARD_URL", "http://localhost:8080") + +# We import bot_instance dynamically or keep a local ref if passed +# Removed top level import to prevent circular import + +def create_access_token(data: dict): + to_encode = data.copy() + expire = time.time() + (ACCESS_TOKEN_EXPIRE_MINUTES * 60) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, JWT_SECRET, algorithm=ALGORITHM) + return encoded_jwt + +def get_current_user(request: Request): + """Dependency to get the current user from the Authorization header.""" + auth_header = request.headers.get("Authorization") + if not auth_header or not auth_header.startswith("Bearer "): + raise HTTPException(status_code=401, detail="Not authenticated") + + token = auth_header.split(" ")[1] + + try: + payload = jwt.decode(token, JWT_SECRET, algorithms=[ALGORITHM]) + user_id: str = payload.get("sub") + if user_id is None: + raise HTTPException(status_code=401, detail="Invalid token") + return {"id": user_id, "username": payload.get("username", ""), "avatar": payload.get("avatar", "")} + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token has expired") + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Invalid token") + +@router.get("/login") +async def login(): + """Generates the Discord OAuth2 Authorization URL and redirects the user.""" + # We want to respond to the dashboard frontend, passing the code back to the frontend. + params = { + "client_id": CLIENT_ID, + "redirect_uri": REDIRECT_URI, + "response_type": "code", + "scope": "identify guilds", + "prompt": "consent" + } + url = f"https://discord.com/oauth2/authorize?{urlencode(params)}" + print(f"[DEBUG] Generated Discord URL: {url}") + return {"url": url} + +@router.post("/callback") +async def callback(request: Request): + """Exchanges code for a token and creates a JWT session.""" + data = await request.json() + code = data.get("code") + + if not code: + raise HTTPException(status_code=400, detail="No code provided") + + # Exchange code for token + async with httpx.AsyncClient() as client: + token_data = { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "authorization_code", + "code": code, + "redirect_uri": REDIRECT_URI + } + + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + try: + token_res = await client.post("https://discord.com/api/oauth2/token", data=token_data, headers=headers) + token_res.raise_for_status() + token_json = token_res.json() + access_token = token_json.get("access_token") + except Exception as e: + raise HTTPException(status_code=400, detail=f"Failed to exchange token: {e}") + + # Get user info + user_res = await client.get("https://discord.com/api/users/@me", headers={ + "Authorization": f"Bearer {access_token}" + }) + user_json = user_res.json() + user_id = user_json.get("id") + + # Verify if user has admin permissions on any guild bot is in (we handle actual guilds in /me) + # For now, just generate JWT + jwt_token = create_access_token({ + "sub": user_id, + "username": user_json.get("username"), + "avatar": user_json.get("avatar") + }) + + return { + "access_token": jwt_token, + "token_type": "bearer", + "user": { + "id": str(user_id), + "username": user_json.get("username"), + "avatar": user_json.get("avatar") + }, + "discord_token": access_token # Send discord token to frontend to fetch guilds + } + +@router.get("/me") +async def get_me(request: Request, user: dict = Depends(get_current_user)): + """Returns the user along with guilds they manage that the bot is also in.""" + from src.api.dashboard.routes import bot_instance + + auth_header = request.headers.get("Authorization") + if not auth_header: + raise HTTPException(status_code=401) + + # In a real app, we'd store the Discord Access Token in a session or database. + # For now, let's assume the client might send it or we fetch it if we had it. + # To make this "really work" without a DB yet, we expect a 'X-Discord-Token' header + # or just use the one from the callback if we were to store it. + + discord_token = request.headers.get("X-Discord-Token") + user_guilds = [] + + if discord_token: + async with httpx.AsyncClient() as client: + guilds_res = await client.get("https://discord.com/api/users/@me/guilds", headers={ + "Authorization": f"Bearer {discord_token}" + }) + if guilds_res.status_code == 200: + all_guilds = guilds_res.json() + for g in all_guilds: + # check permissions (Manage Guild = 0x20) + perms = int(g.get("permissions", 0)) + is_admin = (perms & 0x20) == 0x20 or (perms & 0x8) == 0x8 + + if is_admin: + # Check if bot is in guild + guild_id = int(g.get("id")) + if bot_instance and bot_instance.get_guild(guild_id): + user_guilds.append({ + "id": str(guild_id), + "name": g.get("name"), + "icon": g.get("icon"), + "permissions": perms + }) + + return { + "user": user, + "guilds": user_guilds + } diff --git a/src/api/dashboard/routes.py b/src/api/dashboard/routes.py index a4f3b5a..bdfd4b0 100644 --- a/src/api/dashboard/routes.py +++ b/src/api/dashboard/routes.py @@ -1,11 +1,17 @@ -from fastapi import APIRouter, Request, HTTPException, Security, status +from fastapi import APIRouter, Request, HTTPException, Security, status, Depends from fastapi.security import APIKeyHeader import os +import discord +from src.api.dashboard.auth_routes import get_current_user from typing import List, Optional -from datetime import datetime +from datetime import datetime, timedelta import time # Falls du Schemas nutzt: from .schemas import ServerStatus, UserInfo +from .auth_routes import router as auth_router +from .settings_routes import router as settings_router +from .user_routes import router as user_router + # Wir erstellen einen Router, den wir später in die Haupt-App einbinden router_public = APIRouter( prefix="/v1/managerx", @@ -93,8 +99,194 @@ async def get_api_key(api_key_header: str = Security(API_KEY_HEADER)): router = APIRouter( prefix="/dashboard", - tags=["dashboard"], - dependencies=[Security(get_api_key)] + tags=["dashboard"] ) + +# Public sub-routers (no global X-API-KEY required, they manage their own like JWT) +@router.get("/guilds/{guild_id}/channels") +async def get_guild_channels(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches text channels for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + # Check if user is in guild and has appropriate permissions (Manage Guild or Admin) + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + channels = [ + {"id": str(c.id), "name": c.name} + for c in guild.text_channels + ] + return {"channels": channels} + +@router.get("/guilds/{guild_id}/roles") +async def get_guild_roles(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches manageable roles for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + roles = [ + {"id": str(r.id), "name": r.name, "color": str(r.color)} + for r in guild.roles if not r.is_default() and not r.managed + ] + return {"roles": roles} + +@router.get("/guilds/{guild_id}/categories") +async def get_guild_categories(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches categories for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + categories = [ + {"id": str(c.id), "name": c.name} + for c in guild.categories + ] + return {"categories": categories} + +@router.get("/guilds/{guild_id}/voice_channels") +async def get_guild_voice_channels(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches voice channels for a specific guild.""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + channels = [ + {"id": str(c.id), "name": c.name} + for c in guild.voice_channels + ] + return {"channels": channels} + +@router.get("/guilds/{guild_id}/stats") +async def get_guild_stats(guild_id: int, user: dict = Depends(get_current_user)): + """Fetches server statistics (Daily joins, message count, member total).""" + if bot_instance is None: + raise HTTPException(status_code=503, detail="Bot-Verbindung nicht verfügbar") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found or bot not in guild") + + member = guild.get_member(int(user["id"])) + if not member or not (member.guild_permissions.manage_guild or member.guild_permissions.administrator): + raise HTTPException(status_code=403, detail="Insufficient permissions") + + # Fetch daily growth/activity + today_dt = discord.utils.utcnow() + today_str = today_dt.strftime('%Y-%m-%d') + yesterday_str = (today_dt - timedelta(days=1)).strftime('%Y-%m-%d') + joined_today = 0 + joined_yesterday = 0 + messages_today = 0 + messages_yesterday = 0 + history = [] + + try: + # Pre-fetch histories + welcome_history = [] + stats_history = [] + + if hasattr(bot_instance, 'welcome_db'): + welcome_history = await bot_instance.welcome_db.get_weekly_stats(guild_id) + for day in welcome_history: + if day['date'] == today_str: + joined_today = day['joins'] + elif day['date'] == yesterday_str: + joined_yesterday = day['joins'] + + if hasattr(bot_instance, 'stats_db'): + messages_today = await bot_instance.stats_db.get_daily_messages(guild_id, today_str) + messages_yesterday = await bot_instance.stats_db.get_daily_messages(guild_id, yesterday_str) + stats_history = await bot_instance.stats_db.get_weekly_stats(guild_id) + + # 2. Combine history for the last 7 days + for i in range(6, -1, -1): + date_obj = today_dt - timedelta(days=i) + d_str = date_obj.strftime('%Y-%m-%d') + day_name = date_obj.strftime('%a') + m_count = 0 + j_count = 0 + for h in stats_history: + if h['date'] == d_str: + m_count = h['messages']; break + for h in welcome_history: + if h['date'] == d_str: + j_count = h['joins']; break + history.append({"name": day_name, "messages": m_count, "joins": j_count}) + + # Calculate Trends + def calc_trend(today, yesterday): + if today == yesterday: + return "neutral", "0%" + if yesterday == 0: + return "up", "+100%" + diff = today - yesterday + pct = round((abs(diff) / yesterday) * 100) + return ("up" if diff > 0 else "down"), f"{'+' if diff > 0 else '-'}{pct}%" + + m_trend, m_trend_val = calc_trend(messages_today, messages_yesterday) + j_trend, j_trend_val = calc_trend(joined_today, joined_yesterday) + + # Prepare final stats object + total_members = guild.member_count or len(guild.members) + online_members = 0 + if intents_working := guild.members: + online_members = sum(1 for m in guild.members if m.status != discord.Status.offline) + + stats = { + "total_members": total_members, + "online_members": online_members, + "text_channels": len(guild.text_channels), + "voice_channels": len(guild.voice_channels), + "joined_today": joined_today, + "joined_trend": j_trend, + "joined_trend_value": j_trend_val, + "messages_today": messages_today, + "messages_trend": m_trend, + "messages_trend_value": m_trend_val, + "history": history + } + return stats + except Exception as e: + print(f"Stats error: {e}") + return { + "total_members": guild.member_count, + "online_members": 0, + "text_channels": len(guild.text_channels), + "voice_channels": len(guild.voice_channels), + "joined_today": 0, + "messages_today": 0 + } + +router.include_router(auth_router) +router.include_router(settings_router) +router.include_router(user_router) router.include_router(router_public) diff --git a/src/api/dashboard/settings_routes.py b/src/api/dashboard/settings_routes.py new file mode 100644 index 0000000..c7a086c --- /dev/null +++ b/src/api/dashboard/settings_routes.py @@ -0,0 +1,411 @@ +from fastapi import APIRouter, Request, HTTPException, Security, status, Depends +from src.api.dashboard.auth_routes import get_current_user +from mx_devtools import WelcomeDatabase, AntiSpamDatabase, GlobalChatDatabase, LevelDatabase, LoggingDatabase, AutoDeleteDB, AutoRoleDatabase, TempVCDatabase +import discord +from datetime import datetime + +router = APIRouter( + prefix="/settings", + tags=["settings"], + dependencies=[Depends(get_current_user)] +) + +async def send_dashboard_notification(guild_id: int, module_name: str, user_name: str, channel_id: int = None): + """Helper to send a notification to a Discord channel when settings are saved.""" + from src.api.dashboard.routes import bot_instance + if not bot_instance: + return + + guild = bot_instance.get_guild(guild_id) + if not guild: + return + + # Try to find a suitable channel if none provided + if not channel_id: + # For general settings, we might use a system channel or first available + target_channel = guild.system_channel or guild.text_channels[0] + else: + target_channel = guild.get_channel(channel_id) + + if not target_channel: + return + + embed = discord.Embed( + title="⚙️ Dashboard Einstellungen aktualisiert", + description=f"Die Einstellungen für das Modul **{module_name}** wurden über das Dashboard geändert.", + color=discord.Color.blue(), + timestamp=datetime.now() + ) + embed.add_field(name="Administrator", value=user_name, inline=True) + embed.add_field(name="Modul", value=module_name, inline=True) + embed.set_footer(text="ManagerX Dashboard System", icon_url=bot_instance.user.avatar.url if bot_instance.user.avatar else None) + + try: + await target_channel.send(embed=embed) + except Exception as e: + print(f"Failed to send dashboard notification: {e}") + +@router.get("/{guild_id}") +async def get_settings(guild_id: int): + """Fetch settings for a specific guild.""" + from src.api.dashboard.routes import bot_instance + + if not bot_instance or not hasattr(bot_instance, 'settings_db'): + raise HTTPException(status_code=503, detail="Bot database not ready") + + try: + guild_lang = bot_instance.settings_db.get_guild_language(guild_id) if hasattr(bot_instance.settings_db, 'get_guild_language') else "de" + + return { + "success": True, + "data": { + "bot_name": bot_instance.user.name, + "prefix": "!" , + "auto_mod": True, + "welcome_message": False, + "language": guild_lang + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/{guild_id}") +async def update_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update general settings for a specific guild.""" + from src.api.dashboard.routes import bot_instance + + if not bot_instance or not hasattr(bot_instance, 'settings_db'): + raise HTTPException(status_code=503, detail="Bot database not ready") + + data = await request.json() + + try: + # Update logic... + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Allgemein", user_name) + return {"success": True, "message": "Settings updated"} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save settings: {e}") + +# --- Welcome Module Routes --- + +@router.get("/{guild_id}/channels") +async def get_guild_channels(guild_id: int): + """Returns a list of text channels for the guild.""" + from src.api.dashboard.routes import bot_instance + if not bot_instance: + raise HTTPException(status_code=503, detail="Bot not ready") + + guild = bot_instance.get_guild(guild_id) + if not guild: + raise HTTPException(status_code=404, detail="Guild not found") + + channels = [ + {"id": str(c.id), "name": c.name} + for c in guild.text_channels + ] + return {"success": True, "channels": channels} + +@router.get("/{guild_id}/welcome") +async def get_welcome_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch welcome-specific settings.""" + db = WelcomeDatabase() + try: + settings = await db.get_welcome_settings(guild_id) + if settings and "channel_id" in settings and settings["channel_id"]: + settings["channel_id"] = str(settings["channel_id"]) + if settings and "auto_role_id" in settings and settings["auto_role_id"]: + settings["auto_role_id"] = str(settings["auto_role_id"]) + + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/welcome") +async def update_welcome_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update welcome-specific settings.""" + data = await request.json() + db = WelcomeDatabase() + + if "channel_id" in data and data["channel_id"]: + data["channel_id"] = int(data["channel_id"]) + if "auto_role_id" in data and data["auto_role_id"]: + data["auto_role_id"] = int(data["auto_role_id"]) + + try: + success = await db.update_welcome_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + # Invalidate cache if possible + from src.api.dashboard.routes import bot_instance + if bot_instance: + cog = bot_instance.get_cog("WelcomeSystem") + if cog and hasattr(cog, 'invalidate_cache'): + cog.invalidate_cache(guild_id) + + # Send notification to the welcome channel if configured + channel_id = data.get("channel_id") + await send_dashboard_notification(guild_id, "Welcome System", user_name, channel_id) + + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save welcome settings: {e}") + +# --- AntiSpam Module Routes --- + +@router.get("/{guild_id}/antispam") +async def get_antispam_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch AntiSpam-specific settings.""" + db = AntiSpamDatabase() + try: + settings = db.get_spam_settings(guild_id) + if settings and "log_channel_id" in settings and settings["log_channel_id"]: + settings["log_channel_id"] = str(settings["log_channel_id"]) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/antispam") +async def update_antispam_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update AntiSpam-specific settings.""" + data = await request.json() + db = AntiSpamDatabase() + + if "log_channel_id" in data and data["log_channel_id"]: + data["log_channel_id"] = int(data["log_channel_id"]) + + try: + # Use set_spam_settings with direct kwargs if possible, or mapping + success = db.set_spam_settings( + guild_id, + max_messages=data.get("max_messages", 5), + time_frame=data.get("time_frame", 10), + log_channel_id=data.get("log_channel_id") + ) + if success: + user_name = user.get("username", "Unbekannter User") + from src.api.dashboard.routes import bot_instance + if bot_instance: + cog = bot_instance.get_cog("AntiSpam") + # Add cache invalidation if AntiSpam cog supports it + + await send_dashboard_notification(guild_id, "Anti-Spam", user_name, data.get("log_channel_id")) + + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save AntiSpam settings: {e}") + +# --- GlobalChat Module Routes --- + +@router.get("/{guild_id}/globalchat") +async def get_globalchat_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch GlobalChat-specific settings.""" + db = GlobalChatDatabase() + try: + settings = db.get_guild_settings(guild_id) + channel_id = db.get_globalchat_channel(guild_id) + settings["channel_id"] = str(channel_id) if channel_id else None + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/globalchat") +async def update_globalchat_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update GlobalChat-specific settings.""" + data = await request.json() + db = GlobalChatDatabase() + + try: + success = True + user_name = user.get("username", "Unbekannter User") + + # Handle channel_id separately + new_channel_id = data.get("channel_id") + if new_channel_id: + success = db.set_globalchat_channel(guild_id, int(new_channel_id)) + + # Update other settings + for key in ["filter_enabled", "nsfw_filter", "embed_color"]: + if key in data: + db.update_guild_setting(guild_id, key, data[key]) + + if success: + await send_dashboard_notification(guild_id, "Global Chat", user_name, int(new_channel_id) if new_channel_id else None) + + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save GlobalChat settings: {e}") + +# --- LevelSystem Module Routes --- + +@router.get("/{guild_id}/levels") +async def get_level_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch LevelSystem settings.""" + db = LevelDatabase() + try: + settings = db.get_guild_settings(guild_id) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/levels") +async def update_level_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update LevelSystem settings.""" + data = await request.json() + db = LevelDatabase() + try: + success = db.update_guild_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Level-System", user_name) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save level settings: {e}") + +# --- Logging Module Routes --- + +@router.get("/{guild_id}/logging") +async def get_logging_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch Logging settings.""" + db = LoggingDatabase() + try: + settings = db.get_guild_settings(guild_id) + if settings and "channel_id" in settings and settings["channel_id"]: + settings["channel_id"] = str(settings["channel_id"]) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/logging") +async def update_logging_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update Logging settings.""" + data = await request.json() + db = LoggingDatabase() + + if "channel_id" in data and data["channel_id"]: + data["channel_id"] = int(data["channel_id"]) + + try: + success = db.update_guild_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Server-Log", user_name, data.get("channel_id")) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save logging settings: {e}") + +# --- AutoRole Module Routes --- + +@router.get("/{guild_id}/autorole") +async def get_autorole_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch AutoRole settings.""" + db = AutoRoleDatabase() + try: + settings = db.get_guild_settings(guild_id) + if settings and "role_id" in settings and settings["role_id"]: + settings["role_id"] = str(settings["role_id"]) + return {"success": True, "data": settings or {}} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/autorole") +async def update_autorole_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update AutoRole settings.""" + data = await request.json() + db = AutoRoleDatabase() + + if "role_id" in data and data["role_id"]: + data["role_id"] = int(data["role_id"]) + + try: + success = db.update_guild_settings(guild_id, **data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Auto-Role", user_name) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save autorole settings: {e}") + +# --- AutoDelete Module Routes --- + +@router.get("/{guild_id}/autodelete") +async def get_autodelete_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch AutoDelete settings.""" + db = AutoDeleteDB() + try: + settings = db.get_guild_settings(guild_id) + return {"success": True, "data": settings or []} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/autodelete") +async def update_autodelete_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update AutoDelete settings.""" + data = await request.json() + db = AutoDeleteDB() + try: + # Assuming db.update_guild_settings(guild_id, data) where data is a list of channel configs + success = db.update_guild_settings(guild_id, data) + if success: + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "Auto-Delete", user_name) + return {"success": success} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save autodelete settings: {e}") +# --- TempVC Module Routes --- + +@router.get("/{guild_id}/tempvc") +async def get_tempvc_settings(guild_id: int, user: dict = Depends(get_current_user)): + """Fetch TempVC-specific settings.""" + db = TempVCDatabase() + try: + settings = db.get_tempvc_settings(guild_id) + if settings: + # result is tuple: (creator_channel_id, category_id, auto_delete_time) + data = { + "creator_channel_id": str(settings[0]), + "category_id": str(settings[1]), + "auto_delete_time": settings[2] + } + else: + data = {} + + # Get UI settings + ui_settings = db.get_ui_settings(guild_id) + if ui_settings: + data["ui_enabled"] = bool(ui_settings[0]) + data["ui_prefix"] = ui_settings[1] + else: + data["ui_enabled"] = False + data["ui_prefix"] = "🔧" + + return {"success": True, "data": data} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/{guild_id}/tempvc") +async def update_tempvc_settings(guild_id: int, request: Request, user: dict = Depends(get_current_user)): + """Update TempVC-specific settings.""" + data = await request.json() + db = TempVCDatabase() + + try: + # Update main settings + creator_channel_id = int(data.get("creator_channel_id")) if data.get("creator_channel_id") else 0 + category_id = int(data.get("category_id")) if data.get("category_id") else 0 + auto_delete_time = int(data.get("auto_delete_time", 0)) + + if creator_channel_id and category_id: + db.set_tempvc_settings(guild_id, creator_channel_id, category_id, auto_delete_time) + + # Update UI settings + ui_enabled = bool(data.get("ui_enabled", False)) + ui_prefix = data.get("ui_prefix", "🔧") + db.set_ui_settings(guild_id, ui_enabled, ui_prefix) + + user_name = user.get("username", "Unbekannter User") + await send_dashboard_notification(guild_id, "TempVC System", user_name, creator_channel_id or None) + + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to save TempVC settings: {e}") diff --git a/src/api/dashboard/user_routes.py b/src/api/dashboard/user_routes.py new file mode 100644 index 0000000..e478667 --- /dev/null +++ b/src/api/dashboard/user_routes.py @@ -0,0 +1,47 @@ +from fastapi import APIRouter, Request, HTTPException, Depends +from src.api.dashboard.auth_routes import get_current_user +from mx_devtools import SettingsDB +import discord + +router = APIRouter( + prefix="/user", + tags=["user"], + dependencies=[Depends(get_current_user)] +) + +@router.get("/settings") +async def get_user_settings(user: dict = Depends(get_current_user)): + """Fetch user settings from SettingsDB.""" + settings_db = SettingsDB() + try: + user_id = int(user["id"]) + + # Get language setting from SettingsDB + language = settings_db.get_user_language(user_id) + + return { + "success": True, + "data": { + "user_id": str(user_id), + "language": language, + "username": user.get("username", "Unknown") + } + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Database error: {e}") + +@router.post("/settings") +async def update_user_settings(request: Request, user: dict = Depends(get_current_user)): + """Update user settings in SettingsDB.""" + data = await request.json() + settings_db = SettingsDB() + try: + user_id = int(user["id"]) + + # Update language in SettingsDB if provided + if "language" in data: + settings_db.set_user_language(user_id, data["language"]) + + return {"success": True} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Update failed: {e}") diff --git a/src/bot/cogs/guild/globalchat.py b/src/bot/cogs/guild/globalchat.py index d520db9..5868a06 100644 --- a/src/bot/cogs/guild/globalchat.py +++ b/src/bot/cogs/guild/globalchat.py @@ -942,7 +942,7 @@ async def setup_globalchat( ) container.add_text(feature_text) - view = discord.ui.View(container, timeout=None) + view = discord.ui.DesignerView(container, timeout=None) await ctx.respond(view=view, ephemeral=True) except Exception as e: diff --git a/src/bot/cogs/guild/loggingsystem.py b/src/bot/cogs/guild/loggingsystem.py index fa1e9ec..4b79aba 100644 --- a/src/bot/cogs/guild/loggingsystem.py +++ b/src/bot/cogs/guild/loggingsystem.py @@ -47,7 +47,7 @@ def __init__(self, bot): 'logs_sent': 0, 'errors': 0, 'cache_hits': 0, - 'startup_time': datetime.utcnow(), + 'startup_time': discord.utils.utcnow(), } # Start background tasks @@ -106,7 +106,7 @@ async def _cleanup_caches(self): cleanup_count += 1 # Bulk Delete Cache bereinigen (älter als 5 Minuten) - current_time = datetime.utcnow() + current_time = discord.utils.utcnow() expired_guilds = [] for guild_id, data in self._bulk_deletes.items(): @@ -187,7 +187,7 @@ async def send_log(self, guild_id: int, embed: discord.Embed, log_type: str = "g title="⚠️ Log-Fehler", description="Originale Log-Nachricht konnte nicht angezeigt werden (zu lang oder ungültig)", color=discord.Color.orange(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) await channel.send(embed=fallback_embed) except: @@ -208,7 +208,7 @@ def _create_user_embed(self, title: str, user: discord.User, color: discord.Colo title=title, description=description, color=color, - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) # User Info - immer als erstes @@ -339,7 +339,7 @@ async def set_log_channel(self, ctx, title="🧪 Test-Nachricht", description=f"Log-Channel für **{log_type}** erfolgreich konfiguriert!", color=discord.Color.blue(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) test_embed.set_footer(text="Dies ist eine Test-Nachricht") await self.send_log(ctx.guild.id, test_embed, "general" if log_type == "all" else log_type) @@ -375,7 +375,7 @@ async def remove_log_channel(self, ctx, title="🗑️ Log-Channel entfernt", description=description, color=discord.Color.red(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) embed.set_footer(text=f"Entfernt von {ctx.author}") await ctx.respond(embed=embed, ephemeral=True) @@ -400,7 +400,7 @@ async def log_status(self, ctx): embed = discord.Embed( title="📊 Logging Status", color=discord.Color.blue(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) if not channels: @@ -423,7 +423,7 @@ async def log_status(self, ctx): embed.description = status_text # Bot Statistiken - uptime = datetime.utcnow() - self._stats['startup_time'] + uptime = discord.utils.utcnow() - self._stats['startup_time'] uptime_str = f"{uptime.days}d {uptime.seconds//3600}h {(uptime.seconds%3600)//60}m" embed.add_field( @@ -472,7 +472,7 @@ async def log_status(self, ctx): async def log_backup(self, ctx): """Erstellt ein Datenbank-Backup""" try: - backup_path = f"data/log_channels_backup_{datetime.utcnow().strftime('%Y%m%d_%H%M%S')}.db" + backup_path = f"data/log_channels_backup_{discord.utils.utcnow().strftime('%Y%m%d_%H%M%S')}.db" success = await self.db.backup_database(backup_path) if success: @@ -509,7 +509,7 @@ async def on_member_join(self, member: discord.Member): try: self._stats['events_processed'] += 1 - account_age = datetime.utcnow() - member.created_at + account_age = discord.utils.utcnow() - member.created_at age_days = account_age.days # Verdächtigkeits-Score @@ -519,7 +519,7 @@ async def on_member_join(self, member: discord.Member): elif age_days < 7: suspicious_factors.append("Neues Konto (< 7 Tage)") - if member.display_avatar.is_default(): + if member.avatar is None: suspicious_factors.append("Standard Avatar") # Default Username Pattern @@ -573,7 +573,7 @@ async def on_member_remove(self, member: discord.Member): } if member.joined_at: - duration = datetime.utcnow() - member.joined_at + duration = discord.utils.utcnow() - member.joined_at days = duration.days hours = duration.seconds // 3600 @@ -617,7 +617,7 @@ async def on_message_delete(self, message: discord.Message): guild_id = message.guild.id # Bulk Delete Detection - current_time = datetime.utcnow() + current_time = discord.utils.utcnow() if guild_id not in self._bulk_deletes: self._bulk_deletes[guild_id] = { @@ -646,7 +646,7 @@ async def on_message_delete(self, message: discord.Message): title="🗑️ Bulk-Löschung erkannt", description=f"**{len(bulk_data['messages'])}** Nachrichten wurden in kurzer Zeit gelöscht", color=discord.Color.dark_red(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) # Channel Info @@ -680,7 +680,7 @@ async def on_message_delete(self, message: discord.Message): embed = discord.Embed( title="🗑️ Nachricht gelöscht", color=discord.Color.red(), - timestamp=datetime.utcnow() + timestamp=discord.utils.utcnow() ) # Author Info diff --git a/src/bot/cogs/guild/welcome.py b/src/bot/cogs/guild/welcome.py index 77d93c7..f8973c5 100644 --- a/src/bot/cogs/guild/welcome.py +++ b/src/bot/cogs/guild/welcome.py @@ -64,6 +64,7 @@ def __init__(self, bot): """ self.bot = bot self.db = WelcomeDatabase() + self.bot.welcome_db = self.db # Cache für bessere Performance self._settings_cache = {} self._cache_timeout = 300 # 5 Minuten Cache diff --git a/src/bot/cogs/management/autodelete.py b/src/bot/cogs/management/autodelete.py index 6c79776..711b529 100644 --- a/src/bot/cogs/management/autodelete.py +++ b/src/bot/cogs/management/autodelete.py @@ -21,7 +21,7 @@ def __init__(self, bot): @autodelete.command(name="setup", description="Richtet AutoDelete für einen Kanal ein.") async def setup(self, ctx, channel: Option(discord.TextChannel, "Kanal", required=True), - duration: Option(int, "Zeit in Sekunden (min: 60, max: 604800)", required=True), + duration: Option(int, "Zeit in Sekunden (min: 60s max: 7d (604800s))", required=True), exclude_pinned: Option(bool, "Angepinnte Nachrichten ausschließen", default=True), exclude_bots: Option(bool, "Bot-Nachrichten ausschließen", default=False)): diff --git a/src/bot/cogs/user/stats.py b/src/bot/cogs/user/stats.py index 40d7c63..58300a3 100644 --- a/src/bot/cogs/user/stats.py +++ b/src/bot/cogs/user/stats.py @@ -22,6 +22,7 @@ class EnhancedStatsCog(ezcord.Cog): def __init__(self, bot: commands.Bot): self.bot = bot self.db = StatsDB() + self.bot.stats_db = self.db self.level_db = LevelDatabase() self.cleanup_task.start() self.monthly_reset_task.start() diff --git a/src/bot/core/__init__.py b/src/bot/core/__init__.py index 3a4f213..61d58e5 100644 --- a/src/bot/core/__init__.py +++ b/src/bot/core/__init__.py @@ -11,7 +11,6 @@ from .database import DatabaseManager from .dashboard import DashboardTask from .utils import print_logo, format_uptime, truncate_text -from .constants import * __all__ = [ 'ConfigLoader', diff --git a/src/bot/core/dashboard.py b/src/bot/core/dashboard.py index 38d7864..9b0f39c 100644 --- a/src/bot/core/dashboard.py +++ b/src/bot/core/dashboard.py @@ -7,6 +7,7 @@ """ import json +import discord from datetime import datetime from pathlib import Path from discord.ext import tasks @@ -49,7 +50,7 @@ async def _update_stats(self): "uptime": self._get_uptime(), "python_version": self._get_python_version() }, - "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + "updated_at": discord.utils.utcnow().strftime("%Y-%m-%d %H:%M:%S") } # In Datei schreiben @@ -62,7 +63,7 @@ async def _update_stats(self): def _get_uptime(self) -> str: """Berechnet die Bot-Uptime""" if hasattr(self.bot, 'start_time'): - delta = datetime.now() - self.bot.start_time + delta = discord.utils.utcnow() - self.bot.start_time hours, remainder = divmod(int(delta.total_seconds()), 3600) minutes, seconds = divmod(remainder, 60) return f"{hours}h {minutes}m {seconds}s" @@ -76,7 +77,7 @@ def _get_python_version(self) -> str: def register(self): """Registriert den Task (startet ihn noch nicht)""" # Startzeit speichern - self.bot.start_time = datetime.now() + self.bot.start_time = discord.utils.utcnow() def start(self): diff --git a/src/bot/core/constants.py b/src/scripts/constants.py similarity index 100% rename from src/bot/core/constants.py rename to src/scripts/constants.py diff --git a/src/web/App.tsx b/src/web/App.tsx index 869e4db..c0356f9 100644 --- a/src/web/App.tsx +++ b/src/web/App.tsx @@ -15,10 +15,13 @@ const CommandsPage = lazy(() => import("./pages/CommandsPage")); const TeamPage = lazy(() => import("./pages/TeamPage")); const RoadmapPage = lazy(() => import("./pages/RoadmapPage")); const License = lazy(() => import("./pages/License").then(module => ({ default: module.License }))); +const LoginPage = lazy(() => import("./dashboard/LoginPage")); +const SettingsPage = lazy(() => import("./dashboard/SettingsPage")); +const UserSettingsPage = lazy(() => import("./dashboard/UserSettingsPage")); +const AuthCallback = lazy(() => import("./pages/AuthCallback")); const queryClient = new QueryClient(); -// Loading fallback component const PageLoader = () => (

@@ -40,7 +43,38 @@ const PageLoader = () => (
); -const AppContent = () => { +const DashboardRoutes = () => { + const location = useLocation(); + + return ( + + + }> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); +}; + +const MainRoutes = () => { const location = useLocation(); return ( @@ -70,6 +104,11 @@ const AppContent = () => { } /> } /> } /> + } /> + } /> + } /> + } /> + } /> } /> @@ -78,11 +117,28 @@ const AppContent = () => { ); }; +const AppContent = () => { + const hostname = window.location.hostname; + const isDashboard = hostname.startsWith("dashboard."); + + // Wenn wir auf der Dashboard Subdomain sind + if (isDashboard) { + return ; + } + + // Normale Webseite (Haupt-Domain) + return ; +}; + +import { AuthProvider } from "./components/AuthProvider"; + const App = () => ( - - - + + + + + ); diff --git a/src/web/components/AntiSpamSettings.tsx b/src/web/components/AntiSpamSettings.tsx new file mode 100644 index 0000000..d09914a --- /dev/null +++ b/src/web/components/AntiSpamSettings.tsx @@ -0,0 +1,186 @@ +import React, { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { + Shield, + Hash, + Save, + Clock, + Zap, + AlertTriangle +} from "lucide-react"; +import { useAuth } from "../components/AuthProvider"; +import { toast } from "sonner"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { Switch } from "./ui/switch"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"; + +interface Channel { + id: string; + name: string; +} + +export default function AntiSpamSettings({ guildId }: { guildId: string }) { + const { token } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [channels, setChannels] = useState([]); + + // AntiSpam Form States + const [maxMessages, setMaxMessages] = useState(5); + const [timeFrame, setTimeFrame] = useState(10); + const [logChannelId, setLogChannelId] = useState(""); + + useEffect(() => { + const fetchData = async () => { + if (!token || !guildId) return; + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + + // Fetch Channels + const channelRes = await fetch(`${baseUrl}/dashboard/settings/${guildId}/channels`, { + headers: { "Authorization": `Bearer ${token}` } + }); + if (channelRes.ok) { + const data = await channelRes.json(); + setChannels(data.channels || []); + } + + // Fetch AntiSpam Settings + const settingsRes = await fetch(`${baseUrl}/dashboard/settings/${guildId}/antispam`, { + headers: { "Authorization": `Bearer ${token}` } + }); + if (settingsRes.ok) { + const resData = await settingsRes.json(); + const s = resData.data; + if (s) { + setMaxMessages(s.max_messages ?? 5); + setTimeFrame(s.time_frame ?? 10); + setLogChannelId(s.log_channel_id || ""); + } + } + } catch (e) { + console.error("Fetch error", e); + } + }; + fetchData(); + }, [token, guildId]); + + const handleSave = async () => { + setIsLoading(true); + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const payload = { + max_messages: maxMessages, + time_frame: timeFrame, + log_channel_id: logChannelId + }; + + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/antispam`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + toast.success("AntiSpam-Einstellungen erfolgreich aktualisiert!"); + } else { + throw new Error("Save failed"); + } + } catch (e) { + toast.error("Fehler beim Speichern der AntiSpam-Einstellungen."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + + + Anti-Spam Schutz + + Verhindere Spam-Attacken auf deinem Server. + + + +
+
+ + setMaxMessages(parseInt(e.target.value))} + className="bg-black/20 border-white/10 h-12 rounded-xl" + min={1} + max={50} + /> +

Anzahl der Nachrichten, die in einem Zeitraum erlaubt sind.

+
+ +
+ + setTimeFrame(parseInt(e.target.value))} + className="bg-black/20 border-white/10 h-12 rounded-xl" + min={1} + max={60} + /> +

Der Zeitraum in dem die Nachrichten gezählt werden.

+
+
+ +
+ + +

Hier werden Spam-Vorfälle protokolliert.

+
+ +
+ +

+ Hinweis: Schärfere Einstellungen (weniger Nachrichten/längerer Zeitraum) können dazu führen, dass aktive User fälschlicherweise als Spammer erkannt werden. +

+
+
+
+ +
+ +
+
+ ); +} diff --git a/src/web/components/AuthProvider.tsx b/src/web/components/AuthProvider.tsx new file mode 100644 index 0000000..6e88219 --- /dev/null +++ b/src/web/components/AuthProvider.tsx @@ -0,0 +1,112 @@ +import React, { createContext, useContext, useState, useEffect, ReactNode } from "react"; + +interface User { + id: string; + username: string; + avatar: string | null; +} + +interface AuthContextType { + token: string | null; + user: any | null; + guilds: any[]; + selectedGuildId: string | null; + isAuthenticated: boolean; + login: (token: string, user: any, discordToken?: string) => void; + logout: () => void; + setSelectedGuildId: (id: string) => void; +} + +const AuthContext = createContext(undefined); + +export const AuthProvider = ({ children }: { children: ReactNode }) => { + const [token, setToken] = useState(localStorage.getItem("token")); + const [user, setUser] = useState(JSON.parse(localStorage.getItem("user") || "null")); + const [guilds, setGuilds] = useState([]); + const [selectedGuildId, setSelectedGuildId] = useState(localStorage.getItem("selectedGuildId")); + + const login = (newToken: string, newUser: any, newDiscordToken?: string) => { + setToken(newToken); + setUser(newUser); + localStorage.setItem("token", newToken); + localStorage.setItem("user", JSON.stringify(newUser)); + if (newDiscordToken) { + localStorage.setItem("discord_token", newDiscordToken); + } + }; + + const logout = () => { + setToken(null); + setUser(null); + setGuilds([]); + setSelectedGuildId(null); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("discord_token"); + localStorage.removeItem("selectedGuildId"); + }; + + const handleSetSelectedGuildId = (id: string) => { + setSelectedGuildId(id); + localStorage.setItem("selectedGuildId", id); + }; + + // Fetch guilds and validate session if authenticated + useEffect(() => { + if (token) { + const discordToken = localStorage.getItem("discord_token"); + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + + fetch(`${baseUrl}/dashboard/auth/me`, { + headers: { + "Authorization": `Bearer ${token}`, + "X-Discord-Token": discordToken || "" + } + }) + .then(async (res) => { + if (res.status === 401) { + logout(); + throw new Error("Session expired"); + } + if (!res.ok) throw new Error("Failed to fetch user data"); + return res.json(); + }) + .then(data => { + if (data.user) setUser(data.user); + if (data.guilds) { + setGuilds(data.guilds); + // Select first guild if none selected + if (!selectedGuildId && data.guilds.length > 0) { + handleSetSelectedGuildId(data.guilds[0].id); + } + } + }) + .catch(err => { + console.error("Auth me error:", err); + }); + } + }, [token]); + + return ( + + {children} + + ); +}; + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within an AuthProvider"); + } + return context; +} diff --git a/src/web/components/AutoDeleteSettings.tsx b/src/web/components/AutoDeleteSettings.tsx new file mode 100644 index 0000000..f82ab3a --- /dev/null +++ b/src/web/components/AutoDeleteSettings.tsx @@ -0,0 +1,199 @@ +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card"; +import { Label } from "./ui/label"; +import { Switch } from "./ui/switch"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { + Trash2, + Save, + Clock, + Hash, + Plus, + X, + Search +} from "lucide-react"; +import { toast } from "sonner"; +import { SearchableSelect } from "./ui/SearchableSelect"; + +interface AutoDeleteSettingsProps { + guildId: string; + channels: any[]; +} + +interface ChannelConfig { + channel_id: string; + delay: number; +} + +export default function AutoDeleteSettings({ guildId, channels }: AutoDeleteSettingsProps) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [configs, setConfigs] = useState([]); + + useEffect(() => { + fetchSettings(); + }, [guildId]); + + const fetchSettings = async () => { + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/autodelete`, { + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}` + } + }); + const data = await res.json(); + if (data.success && data.data) { + setConfigs(data.data); + } + } catch (error) { + console.error("Failed to fetch autodelete settings:", error); + toast.error("Fehler beim Laden der Auto-Delete Einstellungen."); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/autodelete`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${localStorage.getItem("token")}` + }, + body: JSON.stringify(configs) + }); + const data = await res.json(); + if (data.success) { + toast.success("Auto-Delete Einstellungen gespeichert! 🧹"); + } + } catch (error) { + console.error("Failed to save autodelete settings:", error); + toast.error("Fehler beim Speichern der Einstellungen."); + } finally { + setSaving(false); + } + }; + + const addChannel = () => { + setConfigs([...configs, { channel_id: "", delay: 60 }]); + }; + + const removeChannel = (index: number) => { + setConfigs(configs.filter((_, i) => i !== index)); + }; + + const updateChannel = (index: number, field: keyof ChannelConfig, value: string | number) => { + const newConfigs = [...configs]; + newConfigs[index] = { ...newConfigs[index], [field]: value }; + setConfigs(newConfigs); + }; + + if (loading) return ( +
+
+
+ ); + + return ( +
+ +
+ + +
+
+
+
+ +
+ Auto-Delete +
+ + Lösche Nachrichten in bestimmten Kanälen automatisch nach einer Zeit. + +
+ +
+
+ + + {configs.length === 0 ? ( +
+

Noch keine Kanäle konfiguriert.

+
+ ) : ( +
+ {configs.map((config, index) => ( +
+
+ + updateChannel(index, "channel_id", val)} + placeholder="Kanal auswählen..." + type="channel" + className="h-10 text-sm" + /> +
+
+ +
+ updateChannel(index, "delay", parseInt(e.target.value))} + className="bg-white/5 border-white/5 focus:border-primary/50 h-10 pl-10" + /> + +
+
+ +
+ ))} +
+ )} + +
+ +
+
+ +
+ ); +} diff --git a/src/web/components/AutoRoleSettings.tsx b/src/web/components/AutoRoleSettings.tsx new file mode 100644 index 0000000..3c25972 --- /dev/null +++ b/src/web/components/AutoRoleSettings.tsx @@ -0,0 +1,187 @@ +/// +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card"; +import { Label } from "./ui/label"; +import { Switch } from "./ui/switch"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { + UserPlus, + Save, + ShieldCheck, + AtSign, + UserCog, + Search +} from "lucide-react"; +import { toast } from "sonner"; +import { SearchableSelect } from "./ui/SearchableSelect"; + +interface AutoRoleSettingsProps { + guildId: string; + roles: any[]; +} + +export default function AutoRoleSettings({ guildId, roles }: AutoRoleSettingsProps) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [settings, setSettings] = useState({ + enabled: false, + role_id: "", + apply_on_join: true, + notify_user: false + }); + + useEffect(() => { + fetchSettings(); + }, [guildId]); + + const fetchSettings = async () => { + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/autorole`, { + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}` + } + }); + const data = await res.json(); + if (data.success && data.data) { + setSettings(prev => ({ ...prev, ...data.data })); + } + } catch (error) { + console.error("Failed to fetch autorole settings:", error); + toast.error("Fehler beim Laden der Auto-Rollen Einstellungen."); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/autorole`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${localStorage.getItem("token")}` + }, + body: JSON.stringify(settings) + }); + const data = await res.json(); + if (data.success) { + toast.success("Auto-Rollen Einstellungen gespeichert! 🛡️"); + } + } catch (error) { + console.error("Failed to save autorole settings:", error); + toast.error("Fehler beim Speichern der Einstellungen."); + } finally { + setSaving(false); + } + }; + + if (loading) return ( +
+
+
+ ); + + return ( +
+ +
+ + +
+
+
+
+ +
+ Auto-Roles +
+ + Weist neuen Mitgliedern automatisch eine Rolle zu. + +
+
+ + setSettings({ ...settings, enabled: checked })} + className="data-[state=checked]:bg-primary" + /> +
+
+
+ + + {/* Role ID */} +
+ + setSettings({ ...settings, role_id: val })} + placeholder="Ziel-Rolle auswählen..." + type="role" + /> +

Stelle sicher, dass die Bot-Rolle über der Ziel-Rolle steht!

+
+ +
+
+
+ +
+

Bei Beitritt

+

Sofort beim Serverbeitritt zuweisen

+
+
+ setSettings({ ...settings, apply_on_join: checked })} + /> +
+ +
+
+ +
+

Nutzer benachrichtigen

+

Sende eine DM nach der Zuweisung

+
+
+ setSettings({ ...settings, notify_user: checked })} + /> +
+
+ +
+ +
+
+ +
+ ); +} diff --git a/src/web/components/GlobalChatSettings.tsx b/src/web/components/GlobalChatSettings.tsx new file mode 100644 index 0000000..38bc0fe --- /dev/null +++ b/src/web/components/GlobalChatSettings.tsx @@ -0,0 +1,188 @@ +import React, { useState, useEffect } from "react"; +import { motion } from "framer-motion"; +import { + Globe, + Hash, + Save, + Shield, + Palette, + Eye +} from "lucide-react"; +import { useAuth } from "../components/AuthProvider"; +import { toast } from "sonner"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { Label } from "./ui/label"; +import { Switch } from "./ui/switch"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"; + +interface Channel { + id: string; + name: string; +} + +export default function GlobalChatSettings({ guildId }: { guildId: string }) { + const { token } = useAuth(); + const [isLoading, setIsLoading] = useState(false); + const [channels, setChannels] = useState([]); + + // GlobalChat Form States + const [channelId, setChannelId] = useState(""); + const [filterEnabled, setFilterEnabled] = useState(true); + const [nsfwFilter, setNsfwFilter] = useState(true); + const [embedColor, setEmbedColor] = useState("#2463eb"); + + useEffect(() => { + const fetchData = async () => { + if (!token || !guildId) return; + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + + // Fetch Channels + const channelRes = await fetch(`${baseUrl}/dashboard/settings/${guildId}/channels`, { + headers: { "Authorization": `Bearer ${token}` } + }); + if (channelRes.ok) { + const data = await channelRes.json(); + setChannels(data.channels || []); + } + + // Fetch GlobalChat Settings + const settingsRes = await fetch(`${baseUrl}/dashboard/settings/${guildId}/globalchat`, { + headers: { "Authorization": `Bearer ${token}` } + }); + if (settingsRes.ok) { + const resData = await settingsRes.json(); + const s = resData.data; + if (s) { + setChannelId(s.channel_id || ""); + setFilterEnabled(s.filter_enabled ?? true); + setNsfwFilter(s.nsfw_filter ?? true); + setEmbedColor(s.embed_color || "#2463eb"); + } + } + } catch (e) { + console.error("Fetch error", e); + } + }; + fetchData(); + }, [token, guildId]); + + const handleSave = async () => { + setIsLoading(true); + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const payload = { + channel_id: channelId, + filter_enabled: filterEnabled, + nsfw_filter: nsfwFilter, + embed_color: embedColor + }; + + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/globalchat`, { + method: "POST", + headers: { + "Authorization": `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + toast.success("Global Chat erfolgreich aktualisiert!"); + } else { + throw new Error("Save failed"); + } + } catch (e) { + toast.error("Fehler beim Speichern der Global Chat Einstellungen."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + + + Global Chat System + + Verbinde deinen Server mit dem weltweiten ManagerX Netzwerk. + + + +
+ + +

Wähle den Kanal, der als Global Chat fungieren soll.

+
+ +
+
+
+ +

Blockiert automatisch Beleidigungen und Links.

+
+ +
+ +
+
+ +

Blockiert nicht jugendfreie Inhalte.

+
+ +
+
+ +
+ +
+ setEmbedColor(e.target.value)} + className="w-12 h-12 p-1 bg-transparent border-none cursor-pointer" + /> + setEmbedColor(e.target.value)} + className="bg-black/20 border-white/10 h-12 rounded-xl font-mono flex-1" + /> +
+

Die Farbe, in der deine Nachrichten global angezeigt werden.

+
+
+
+ +
+ +
+
+ ); +} diff --git a/src/web/components/GuildSelector.tsx b/src/web/components/GuildSelector.tsx new file mode 100644 index 0000000..78e178f --- /dev/null +++ b/src/web/components/GuildSelector.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { useAuth } from "./AuthProvider"; +import { ChevronDown, Server } from "lucide-react"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; + +export default function GuildSelector() { + const { guilds, selectedGuildId, setSelectedGuildId } = useAuth(); + + const selectedGuild = guilds.find(g => g.id === selectedGuildId); + + if (guilds.length === 0) return null; + + return ( + + + + + + + + + Deine Server + + + {guilds.map((guild) => ( + setSelectedGuildId(guild.id)} + > +
+ {guild.icon ? ( + + ) : ( +
+ {guild.name.charAt(0)} +
+ )} +
+ {guild.name} +
+ ))} + + {guilds.length === 0 && ( +
+

Keine Server mit Bot gefunden.

+
+ )} +
+
+
+ ); +} diff --git a/src/web/components/LevelSettings.tsx b/src/web/components/LevelSettings.tsx new file mode 100644 index 0000000..8319b80 --- /dev/null +++ b/src/web/components/LevelSettings.tsx @@ -0,0 +1,237 @@ +/// +import React, { useState, useEffect } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "./ui/card"; +import { Label } from "./ui/label"; +import { Switch } from "./ui/switch"; +import { Input } from "./ui/input"; +import { Button } from "./ui/button"; +import { + Trophy, + Save, + MessageSquare, + Hash, + Volume2, + Ban, + Sparkles, + BarChart3, + Search +} from "lucide-react"; +import { toast } from "sonner"; +import { cn } from "../lib/utils"; +import { SearchableSelect } from "./ui/SearchableSelect"; + +interface LevelSettingsProps { + guildId: string; + channels: any[]; +} + +export default function LevelSettings({ guildId, channels }: LevelSettingsProps) { + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [settings, setSettings] = useState({ + enabled: false, + xp_rate: 1.0, + level_up_message: "Glückwunsch {user}, du bist nun Level {level}!", + level_up_channel: "", + voice_xp: true, + announcement_enabled: true + }); + + useEffect(() => { + fetchSettings(); + }, [guildId]); + + const fetchSettings = async () => { + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/levels`, { + headers: { + "Authorization": `Bearer ${localStorage.getItem("token")}` + } + }); + const data = await res.json(); + if (data.success && data.data) { + setSettings(prev => ({ ...prev, ...data.data })); + } + } catch (error) { + console.error("Failed to fetch level settings:", error); + toast.error("Fehler beim Laden der Level-Einstellungen."); + } finally { + setLoading(false); + } + }; + + const handleSave = async () => { + setSaving(true); + try { + const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8040'; + const res = await fetch(`${baseUrl}/dashboard/settings/${guildId}/levels`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${localStorage.getItem("token")}` + }, + body: JSON.stringify(settings) + }); + const data = await res.json(); + if (data.success) { + toast.success("Level-System Einstellungen gespeichert! ✨"); + } else { + throw new Error(data.detail || "Unbekannter Fehler"); + } + } catch (error) { + console.error("Failed to save level settings:", error); + toast.error("Fehler beim Speichern der Einstellungen."); + } finally { + setSaving(false); + } + }; + + if (loading) return ( +
+
+
+ ); + + return ( +
+ +
+ + +
+
+
+
+ +
+ Level-System +
+ + Belohne aktive Mitglieder deiner Community mit XP und Leveln. + +
+
+ + setSettings({ ...settings, enabled: checked })} + className="data-[state=checked]:bg-primary" + /> +
+
+
+ + +
+ {/* XP Rate */} +
+ +
+ setSettings({ ...settings, xp_rate: parseFloat(e.target.value) })} + className="bg-white/5 border-white/10 focus:border-primary/50 transition-all rounded-xl h-12 pl-4" + /> +
+
+

Standard ist 1.0. Höhere Werte geben mehr XP.

+
+ + {/* Level Up Channel */} +
+ +
+ setSettings({ ...settings, level_up_channel: val })} + placeholder="Aktueller Kanal" + type="channel" + /> +
+
+
+ + {/* Level Up Message */} +
+ +
+