Dashboard System - openguard-bot/openguard GitHub Wiki

Dashboard System

The AIMod Dashboard is a comprehensive web-based management interface built with FastAPI backend and React frontend. It provides server administrators with powerful tools to configure, monitor, and manage their Discord bot.

🌐 Architecture Overview

Technology Stack

Backend:

  • FastAPI - Modern, fast web framework
  • SQLAlchemy - Database ORM
  • Pydantic - Data validation and serialization
  • Discord OAuth2 - Authentication
  • JWT - Session management

Frontend:

  • React 18 - Modern UI framework
  • TypeScript - Type-safe JavaScript
  • Tailwind CSS - Utility-first styling
  • Axios - HTTP client
  • React Router - Client-side routing

System Architecture

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   React App     │    │   FastAPI       │    │   PostgreSQL    │
│   (Frontend)    │◄──►│   (Backend)     │◄──►│   Database      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
         │                       │                       │
         │                       │                       │
         ▼                       ▼                       ▼
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Nginx         │    │   Discord API   │    │   Redis Cache   │
│   (Reverse      │    │   (OAuth2)      │    │   (Sessions)    │
│    Proxy)       │    │                 │    │                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘

🔧 Backend API

FastAPI Application Structure

# main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app import api

app = FastAPI(title="AIMod Dashboard API", version="2.0.0")

# CORS configuration
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(api.router, prefix="/api")

Authentication System

Discord OAuth2 Flow:

@router.get("/login")
async def login():
    """Redirects to Discord for authentication."""
    return RedirectResponse(
        f"{DISCORD_API_URL}/oauth2/authorize"
        f"?client_id={DISCORD_CLIENT_ID}"
        f"&redirect_uri={DISCORD_REDIRECT_URI}"
        f"&response_type=code"
        f"&scope=identify guilds"
    )

@router.get("/callback")
async def callback(code: str):
    """Handles the callback from Discord."""
    # Exchange code for access token
    token_data = await exchange_code_for_token(code)
    
    # Get user information
    user_data = await get_discord_user(token_data["access_token"])
    
    # Create JWT token
    jwt_token = create_jwt_token(user_data)
    
    # Set secure cookie
    response = RedirectResponse("/dashboard")
    response.set_cookie(
        "access_token",
        jwt_token,
        httponly=True,
        secure=True,
        samesite="lax"
    )
    return response

JWT Token Management:

def create_jwt_token(user_data: dict) -> str:
    """Create JWT token for user session."""
    payload = {
        "user_id": user_data["id"],
        "username": user_data["username"],
        "exp": datetime.utcnow() + timedelta(hours=24)
    }
    return jwt.encode(payload, JWT_SECRET, algorithm="HS256")

async def get_current_user(request: Request) -> schemas.User:
    """Get current authenticated user."""
    token = request.cookies.get("access_token")
    if not token:
        raise HTTPException(status_code=401, detail="Not authenticated")
    
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
        return schemas.User(**payload)
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

API Endpoints

Guild Management

Get User Guilds:

@router.get("/guilds", response_model=List[schemas.Guild])
async def get_user_guilds(current_user: schemas.User = Depends(get_current_user)):
    """Get guilds where user has admin permissions."""
    guilds = await get_discord_guilds(current_user.access_token)
    admin_guilds = [g for g in guilds if g.permissions & 0x8]  # Administrator permission
    return admin_guilds

Get Guild Configuration:

@router.get("/guilds/{guild_id}/config", response_model=schemas.ComprehensiveGuildConfig)
async def get_guild_config(
    guild_id: int,
    has_admin: bool = Depends(has_admin_permissions)
):
    """Get complete guild configuration."""
    if not has_admin:
        raise HTTPException(status_code=403, detail="Insufficient permissions")
    
    config = await crud.get_comprehensive_guild_config(guild_id)
    return config

Update Guild Configuration:

@router.put("/guilds/{guild_id}/config")
async def update_guild_config(
    guild_id: int,
    config: schemas.GuildConfigUpdate,
    has_admin: bool = Depends(has_admin_permissions)
):
    """Update guild configuration."""
    if not has_admin:
        raise HTTPException(status_code=403, detail="Insufficient permissions")
    
    await crud.update_guild_config(guild_id, config.dict(exclude_unset=True))
    return {"status": "success"}

Analytics and Statistics

Guild Statistics:

@router.get("/guilds/{guild_id}/stats", response_model=schemas.GuildStats)
async def get_guild_stats(guild_id: int):
    """Get guild statistics and analytics."""
    stats = await crud.get_guild_statistics(guild_id)
    return stats

Command Analytics:

@router.get("/guilds/{guild_id}/analytics/commands", response_model=schemas.CommandAnalytics)
async def get_command_analytics(guild_id: int, days: int = 30):
    """Get command usage analytics."""
    analytics = await crud.get_command_analytics(guild_id, days)
    return analytics

Moderation Analytics:

@router.get("/guilds/{guild_id}/analytics/moderation", response_model=schemas.ModerationAnalytics)
async def get_moderation_analytics(guild_id: int, days: int = 30):
    """Get moderation action analytics."""
    analytics = await crud.get_moderation_analytics(guild_id, days)
    return analytics

User Management

User Profile:

@router.get("/users/{user_id}/profile", response_model=schemas.UserProfile)
async def get_user_profile(user_id: int, guild_id: Optional[int] = None):
    """Get detailed user profile including infractions."""
    profile = await crud.get_user_profile(user_id, guild_id)
    return profile

User Infractions:

@router.get("/guilds/{guild_id}/users/{user_id}/infractions")
async def get_user_infractions(guild_id: int, user_id: int):
    """Get user's infraction history."""
    infractions = await crud.get_user_infractions(guild_id, user_id)
    return infractions

Moderation Actions

Create Moderation Action:

@router.post("/guilds/{guild_id}/moderation/action")
async def create_moderation_action(
    guild_id: int,
    action: schemas.ModerationAction,
    has_admin: bool = Depends(has_admin_permissions)
):
    """Create a new moderation action."""
    if not has_admin:
        raise HTTPException(status_code=403, detail="Insufficient permissions")
    
    result = await crud.create_moderation_action(guild_id, action)
    return result

Data Schemas

Pydantic Models:

class Guild(BaseModel):
    id: str
    name: str
    icon: Optional[str]
    owner: bool
    permissions: int

class GuildConfig(BaseModel):
    enabled: bool = True
    ai_model: str = "github_copilot/gpt-4.1"
    confidence_threshold: int = 70
    rules_text: str = ""
    prefix: str = "!"

class ModerationSettings(BaseModel):
    auto_timeout_enabled: bool = True
    timeout_duration: int = 3600
    auto_ban_enabled: bool = False
    ban_threshold: int = 3
    delete_messages: bool = True

class UserProfile(BaseModel):
    user_id: int
    username: str
    avatar_url: Optional[str]
    total_infractions: int
    recent_infractions: List[dict]
    join_date: Optional[datetime]
    last_seen: Optional[datetime]

🎨 Frontend Application

React Application Structure

src/
├── components/          # Reusable UI components
│   ├── common/         # Generic components
│   ├── forms/          # Form components
│   ├── charts/         # Chart components
│   └── modals/         # Modal dialogs
├── pages/              # Page components
│   ├── Dashboard.js    # Main dashboard
│   ├── GuildConfig.js  # Guild configuration
│   ├── Analytics.js    # Analytics page
│   └── UserProfile.js  # User management
├── hooks/              # Custom React hooks
├── contexts/           # React contexts
├── services/           # API service layer
└── utils/              # Utility functions

Main Dashboard Component

// Dashboard.js
import React, { useState, useEffect } from 'react';
import { useAuth } from '../contexts/AuthContext';
import GuildSelector from '../components/GuildSelector';
import StatsOverview from '../components/StatsOverview';
import RecentActivity from '../components/RecentActivity';

const Dashboard = () => {
    const { user } = useAuth();
    const [selectedGuild, setSelectedGuild] = useState(null);
    const [stats, setStats] = useState(null);

    useEffect(() => {
        if (selectedGuild) {
            fetchGuildStats(selectedGuild.id);
        }
    }, [selectedGuild]);

    const fetchGuildStats = async (guildId) => {
        try {
            const response = await api.get(`/guilds/${guildId}/stats`);
            setStats(response.data);
        } catch (error) {
            console.error('Failed to fetch stats:', error);
        }
    };

    return (
        <div className="min-h-screen bg-gray-100">
            <header className="bg-white shadow">
                <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
                    <div className="flex justify-between items-center py-6">
                        <h1 className="text-3xl font-bold text-gray-900">
                            AIMod Dashboard
                        </h1>
                        <GuildSelector 
                            onGuildSelect={setSelectedGuild}
                            selectedGuild={selectedGuild}
                        />
                    </div>
                </div>
            </header>

            <main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
                {selectedGuild && stats ? (
                    <>
                        <StatsOverview stats={stats} />
                        <RecentActivity guildId={selectedGuild.id} />
                    </>
                ) : (
                    <div className="text-center py-12">
                        <p className="text-gray-500">
                            Select a guild to view dashboard
                        </p>
                    </div>
                )}
            </main>
        </div>
    );
};

export default Dashboard;

Guild Configuration Page

// GuildConfig.js
import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import ConfigSection from '../components/ConfigSection';
import SaveButton from '../components/SaveButton';

const GuildConfig = () => {
    const { guildId } = useParams();
    const [config, setConfig] = useState(null);
    const [loading, setLoading] = useState(true);
    const [saving, setSaving] = useState(false);

    useEffect(() => {
        fetchConfig();
    }, [guildId]);

    const fetchConfig = async () => {
        try {
            const response = await api.get(`/guilds/${guildId}/config`);
            setConfig(response.data);
        } catch (error) {
            console.error('Failed to fetch config:', error);
        } finally {
            setLoading(false);
        }
    };

    const saveConfig = async () => {
        setSaving(true);
        try {
            await api.put(`/guilds/${guildId}/config`, config);
            // Show success message
        } catch (error) {
            console.error('Failed to save config:', error);
            // Show error message
        } finally {
            setSaving(false);
        }
    };

    if (loading) return <div>Loading...</div>;

    return (
        <div className="max-w-4xl mx-auto py-6">
            <div className="bg-white shadow rounded-lg">
                <div className="px-6 py-4 border-b">
                    <h2 className="text-2xl font-bold">Guild Configuration</h2>
                </div>
                
                <div className="p-6 space-y-6">
                    <ConfigSection
                        title="General Settings"
                        config={config.general}
                        onChange={(updates) => 
                            setConfig(prev => ({
                                ...prev,
                                general: { ...prev.general, ...updates }
                            }))
                        }
                    />
                    
                    <ConfigSection
                        title="AI Moderation"
                        config={config.moderation}
                        onChange={(updates) => 
                            setConfig(prev => ({
                                ...prev,
                                moderation: { ...prev.moderation, ...updates }
                            }))
                        }
                    />
                    
                    <ConfigSection
                        title="Security Features"
                        config={config.security}
                        onChange={(updates) => 
                            setConfig(prev => ({
                                ...prev,
                                security: { ...prev.security, ...updates }
                            }))
                        }
                    />
                </div>
                
                <div className="px-6 py-4 border-t bg-gray-50">
                    <SaveButton 
                        onClick={saveConfig}
                        loading={saving}
                    />
                </div>
            </div>
        </div>
    );
};

export default GuildConfig;

API Service Layer

// services/api.js
import axios from 'axios';

const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api';

const api = axios.create({
    baseURL: API_BASE_URL,
    withCredentials: true,
    headers: {
        'Content-Type': 'application/json',
    },
});

// Request interceptor for auth
api.interceptors.request.use(
    (config) => {
        // Add any auth headers if needed
        return config;
    },
    (error) => Promise.reject(error)
);

// Response interceptor for error handling
api.interceptors.response.use(
    (response) => response,
    (error) => {
        if (error.response?.status === 401) {
            // Redirect to login
            window.location.href = '/login';
        }
        return Promise.reject(error);
    }
);

export default api;

🔒 Security Features

Authentication & Authorization

JWT Token Security:

  • HttpOnly cookies prevent XSS attacks
  • Secure flag for HTTPS-only transmission
  • Short expiration times (24 hours)
  • Automatic token refresh

Permission Validation:

async def has_admin_permissions(
    guild_id: int,
    current_user: schemas.User = Depends(get_current_user)
) -> bool:
    """Check if user has admin permissions for guild."""
    user_guilds = await get_discord_guilds(current_user.access_token)
    guild = next((g for g in user_guilds if g.id == str(guild_id)), None)
    
    if not guild:
        raise HTTPException(status_code=404, detail="Guild not found")
    
    # Check for administrator permission (0x8)
    return bool(guild.permissions & 0x8)

Input Validation

Pydantic Validation:

class GuildConfigUpdate(BaseModel):
    enabled: Optional[bool] = None
    ai_model: Optional[str] = Field(None, regex=r'^[a-zA-Z0-9_/-]+$')
    confidence_threshold: Optional[int] = Field(None, ge=0, le=100)
    rules_text: Optional[str] = Field(None, max_length=5000)
    
    @validator('ai_model')
    def validate_ai_model(cls, v):
        allowed_models = ['github_copilot/gpt-4.1', 'openai/gpt-4', 'anthropic/claude-3']
        if v and v not in allowed_models:
            raise ValueError('Invalid AI model')
        return v

Rate Limiting

API Rate Limiting:

from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address

limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@router.get("/guilds/{guild_id}/config")
@limiter.limit("30/minute")
async def get_guild_config(request: Request, guild_id: int):
    # Endpoint implementation
    pass

🚀 Deployment

Production Configuration

Nginx Configuration:

server {
    listen 80;
    server_name dashboard.aimod.example.com;
    
    # Redirect to HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name dashboard.aimod.example.com;
    
    ssl_certificate /path/to/certificate.crt;
    ssl_certificate_key /path/to/private.key;
    
    # Frontend static files
    location / {
        root /var/www/aimod-dashboard;
        try_files $uri $uri/ /index.html;
    }
    
    # API proxy
    location /api/ {
        proxy_pass http://localhost:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Docker Configuration:

# Backend Dockerfile
FROM python:3.11-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

COPY . .
EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Frontend Dockerfile
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

Environment Variables

# Backend Environment
DATABASE_URL=postgresql://user:pass@localhost:5432/aimod_bot
REDIS_URL=redis://localhost:6379
DISCORD_CLIENT_ID=your_client_id
DISCORD_CLIENT_SECRET=your_client_secret
DISCORD_REDIRECT_URI=https://dashboard.example.com/api/callback
JWT_SECRET=your_jwt_secret
ENCRYPTION_KEY=your_encryption_key

# Frontend Environment
REACT_APP_API_URL=https://dashboard.example.com/api
REACT_APP_DISCORD_CLIENT_ID=your_client_id

Next: Cogs and Commands - Complete documentation of all bot cogs and commands

⚠️ **GitHub.com Fallback** ⚠️