Dashboard System - openguard-bot/openguard GitHub Wiki
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.
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
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ React App │ │ FastAPI │ │ PostgreSQL │
│ (Frontend) │◄──►│ (Backend) │◄──►│ Database │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Nginx │ │ Discord API │ │ Redis Cache │
│ (Reverse │ │ (OAuth2) │ │ (Sessions) │
│ Proxy) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
# 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")
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")
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"}
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 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
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
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]
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
// 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;
// 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;
// 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;
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)
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
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
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
# 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