From 34c128fd87a672fc8b70f05404897bc1703876e8 Mon Sep 17 00:00:00 2001 From: PEDZEO Date: Fri, 16 Jan 2026 16:03:51 +0300 Subject: [PATCH] =?UTF-8?q?=20=D0=98=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC=D1=8B?= =?UTF-8?q?=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3?= =?UTF-8?q?=D0=B0=20=D0=B1=D0=B0=D0=BD=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 9 + src/api/banSystem.ts | 272 +++++++++++++ src/locales/en.json | 173 ++++++++- src/locales/ru.json | 173 ++++++++- src/pages/AdminBanSystem.tsx | 722 +++++++++++++++++++++++++++++++++++ src/pages/AdminPanel.tsx | 13 + 6 files changed, 1358 insertions(+), 4 deletions(-) create mode 100644 src/api/banSystem.ts create mode 100644 src/pages/AdminBanSystem.tsx diff --git a/src/App.tsx b/src/App.tsx index ae5c905..3c010d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,7 @@ import AdminTariffs from './pages/AdminTariffs' import AdminServers from './pages/AdminServers' import AdminPanel from './pages/AdminPanel' import AdminDashboard from './pages/AdminDashboard' +import AdminBanSystem from './pages/AdminBanSystem' function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuthStore() @@ -217,6 +218,14 @@ function App() { } /> + + + + } + /> {/* Catch all */} } /> diff --git a/src/api/banSystem.ts b/src/api/banSystem.ts new file mode 100644 index 0000000..18137fd --- /dev/null +++ b/src/api/banSystem.ts @@ -0,0 +1,272 @@ +import apiClient from './client' + +// === Types === + +export interface BanSystemStatus { + enabled: boolean + configured: boolean +} + +export interface BanSystemStats { + total_users: number + active_users: number + users_over_limit: number + total_requests: number + total_punishments: number + active_punishments: number + nodes_online: number + nodes_total: number + agents_online: number + agents_total: number + panel_connected: boolean + uptime_seconds: number | null +} + +export interface BanUserIPInfo { + ip: string + first_seen: string | null + last_seen: string | null + node: string | null + request_count: number + country_code: string | null + country_name: string | null + city: string | null +} + +export interface BanUserRequestLog { + timestamp: string + source_ip: string + destination: string | null + dest_port: number | null + protocol: string | null + action: string | null + node: string | null +} + +export interface BanUserListItem { + email: string + unique_ip_count: number + total_requests: number + limit: number | null + is_over_limit: boolean + blocked_count: number + last_seen: string | null +} + +export interface BanUsersListResponse { + users: BanUserListItem[] + total: number + offset: number + limit: number +} + +export interface BanUserDetailResponse { + email: string + unique_ip_count: number + total_requests: number + limit: number | null + is_over_limit: boolean + blocked_count: number + ips: BanUserIPInfo[] + recent_requests: BanUserRequestLog[] + network_type: string | null +} + +export interface BanPunishmentItem { + id: number | null + user_id: string + uuid: string | null + username: string + reason: string | null + punished_at: string + enable_at: string | null + ip_count: number + limit: number + enabled: boolean + enabled_at: string | null + node_name: string | null +} + +export interface BanPunishmentsListResponse { + punishments: BanPunishmentItem[] + total: number +} + +export interface BanHistoryResponse { + items: BanPunishmentItem[] + total: number +} + +export interface BanUserRequest { + username: string + minutes: number + reason?: string +} + +export interface UnbanResponse { + success: boolean + message: string +} + +export interface BanNodeItem { + name: string + address: string | null + is_connected: boolean + last_seen: string | null + users_count: number + agent_stats: Record | null +} + +export interface BanNodesListResponse { + nodes: BanNodeItem[] + total: number + online: number +} + +export interface BanAgentItem { + node_name: string + sent_total: number + dropped_total: number + batches_total: number + reconnects: number + failures: number + queue_size: number + queue_max: number + dedup_checked: number + dedup_skipped: number + filter_checked: number + filter_filtered: number + health: string + is_online: boolean + last_report: string | null +} + +export interface BanAgentsSummary { + total_agents: number + online_agents: number + total_sent: number + total_dropped: number + avg_queue_size: number + healthy_count: number + warning_count: number + critical_count: number +} + +export interface BanAgentsListResponse { + agents: BanAgentItem[] + summary: BanAgentsSummary | null + total: number + online: number +} + +export interface BanTrafficViolationItem { + id: number | null + username: string + email: string | null + violation_type: string + description: string | null + bytes_used: number + bytes_limit: number + detected_at: string + resolved: boolean +} + +export interface BanTrafficViolationsResponse { + violations: BanTrafficViolationItem[] + total: number +} + +// === API === + +export const banSystemApi = { + // Status + getStatus: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/status') + return response.data + }, + + // Stats + getStats: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/stats') + return response.data + }, + + // Users + getUsers: async (params: { + offset?: number + limit?: number + status?: string + } = {}): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/users', { params }) + return response.data + }, + + getUsersOverLimit: async (limit: number = 50): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/users/over-limit', { + params: { limit } + }) + return response.data + }, + + searchUsers: async (query: string): Promise => { + const response = await apiClient.get(`/cabinet/admin/ban-system/users/search/${encodeURIComponent(query)}`) + return response.data + }, + + getUser: async (email: string): Promise => { + const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}`) + return response.data + }, + + // Punishments + getPunishments: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/punishments') + return response.data + }, + + unbanUser: async (userId: string): Promise => { + const response = await apiClient.post(`/cabinet/admin/ban-system/punishments/${userId}/unban`) + return response.data + }, + + banUser: async (data: BanUserRequest): Promise => { + const response = await apiClient.post('/cabinet/admin/ban-system/ban', data) + return response.data + }, + + getPunishmentHistory: async (query: string, limit: number = 20): Promise => { + const response = await apiClient.get(`/cabinet/admin/ban-system/history/${encodeURIComponent(query)}`, { + params: { limit } + }) + return response.data + }, + + // Nodes + getNodes: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/nodes') + return response.data + }, + + // Agents + getAgents: async (params: { + search?: string + health?: string + status?: string + } = {}): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params }) + return response.data + }, + + getAgentsSummary: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/agents/summary') + return response.data + }, + + // Traffic violations + getTrafficViolations: async (limit: number = 50): Promise => { + const response = await apiClient.get('/cabinet/admin/ban-system/traffic/violations', { + params: { limit } + }) + return response.data + }, +} diff --git a/src/locales/en.json b/src/locales/en.json index 3c1449a..cd9293d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -439,7 +439,8 @@ "apps": "Apps", "wheel": "Wheel", "tariffs": "Tariffs", - "servers": "Servers" + "servers": "Servers", + "banSystem": "Ban Monitoring" }, "panel": { "title": "Admin Panel", @@ -450,7 +451,8 @@ "appsDesc": "Manage connection apps", "wheelDesc": "Configure fortune wheel and prizes", "tariffsDesc": "Manage tariff plans", - "serversDesc": "Configure VPN servers" + "serversDesc": "Configure VPN servers", + "banSystemDesc": "Ban monitoring and violations" }, "wheel": { "title": "Fortune Wheel Settings", @@ -744,6 +746,173 @@ "noTariffs": "No tariffs" } }, + "banSystem": { + "title": "Ban Monitoring", + "subtitle": "BedolagaBan system management", + "notConfigured": "Ban system is not configured", + "notConfiguredDesc": "Set BAN_SYSTEM_API_URL and BAN_SYSTEM_API_TOKEN in configuration", + "loadError": "Failed to load data", + "tabs": { + "dashboard": "Dashboard", + "users": "Users", + "punishments": "Bans", + "nodes": "Nodes", + "agents": "Agents", + "violations": "Violations" + }, + "dashboard": { + "totalUsers": "Total Users", + "activeUsers": "Active Users", + "usersOverLimit": "Over Limit", + "totalRequests": "Total Requests", + "totalPunishments": "Total Punishments", + "activePunishments": "Active Bans", + "nodesOnline": "Nodes Online", + "agentsOnline": "Agents Online", + "panelConnected": "Panel Connected", + "panelDisconnected": "Panel Disconnected", + "uptime": "Uptime" + }, + "users": { + "title": "Users", + "searchPlaceholder": "Search by email...", + "email": "Email", + "uniqueIps": "Unique IPs", + "requests": "Requests", + "limit": "Limit", + "status": "Status", + "lastSeen": "Last Seen", + "overLimit": "Over Limit", + "normal": "Normal", + "noLimit": "No Limit", + "noUsers": "No users found", + "viewDetails": "View Details", + "filter": { + "all": "All", + "overLimit": "Over Limit", + "normal": "Normal" + } + }, + "userDetail": { + "title": "User Details", + "email": "Email", + "uniqueIps": "Unique IPs", + "totalRequests": "Total Requests", + "limit": "Device Limit", + "status": "Status", + "networkType": "Network Type", + "ipHistory": "IP History", + "recentRequests": "Recent Requests", + "ip": "IP Address", + "firstSeen": "First Seen", + "lastSeen": "Last Seen", + "node": "Node", + "requestCount": "Requests", + "country": "Country", + "city": "City", + "timestamp": "Timestamp", + "sourceIp": "Source IP", + "destination": "Destination", + "port": "Port", + "protocol": "Protocol", + "action": "Action", + "noIps": "No IP data", + "noRequests": "No request data", + "ban": "Ban", + "close": "Close" + }, + "punishments": { + "title": "Active Bans", + "username": "User", + "reason": "Reason", + "punishedAt": "Banned At", + "enableAt": "Unban At", + "ipCount": "IPs", + "limit": "Limit", + "node": "Node", + "actions": "Actions", + "unban": "Unban", + "noPunishments": "No active bans", + "unbanConfirm": "Unban user {{username}}?", + "unbanSuccess": "User unbanned", + "unbanError": "Unban failed", + "history": "Ban History", + "searchHistory": "Search history...", + "noHistory": "No history found" + }, + "banModal": { + "title": "Ban User", + "username": "Username", + "usernamePlaceholder": "Enter username", + "duration": "Duration (minutes)", + "durationPlaceholder": "Enter duration in minutes", + "reason": "Reason", + "reasonPlaceholder": "Enter ban reason (optional)", + "cancel": "Cancel", + "ban": "Ban", + "success": "User banned", + "error": "Ban failed" + }, + "nodes": { + "title": "Nodes", + "name": "Name", + "address": "Address", + "status": "Status", + "lastSeen": "Last Seen", + "usersCount": "Users", + "online": "Online", + "offline": "Offline", + "noNodes": "No nodes found", + "total": "Total", + "onlineCount": "Online" + }, + "agents": { + "title": "Agents", + "nodeName": "Node", + "status": "Status", + "health": "Health", + "sent": "Sent", + "dropped": "Dropped", + "batches": "Batches", + "reconnects": "Reconnects", + "failures": "Failures", + "queue": "Queue", + "dedup": "Deduplication", + "filter": "Filtering", + "lastReport": "Last Report", + "online": "Online", + "offline": "Offline", + "healthy": "Healthy", + "warning": "Warning", + "critical": "Critical", + "noAgents": "No agents found", + "summary": { + "title": "Summary", + "totalAgents": "Total Agents", + "onlineAgents": "Online", + "totalSent": "Total Sent", + "totalDropped": "Total Dropped", + "avgQueueSize": "Avg Queue Size", + "healthyCount": "Healthy", + "warningCount": "Warning", + "criticalCount": "Critical" + } + }, + "violations": { + "title": "Traffic Violations", + "username": "User", + "email": "Email", + "type": "Type", + "description": "Description", + "bytesUsed": "Used", + "bytesLimit": "Limit", + "detectedAt": "Detected At", + "resolved": "Resolved", + "noViolations": "No violations found", + "yes": "Yes", + "no": "No" + } + }, "profile": { "title": "Profile", "accountInfo": "Account Information", diff --git a/src/locales/ru.json b/src/locales/ru.json index c070a8b..6f07e37 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -439,7 +439,8 @@ "apps": "Приложения", "wheel": "Колесо", "tariffs": "Тарифы", - "servers": "Серверы" + "servers": "Серверы", + "banSystem": "Мониторинг банов" }, "panel": { "title": "Панель администратора", @@ -450,7 +451,8 @@ "appsDesc": "Управление приложениями для подключения", "wheelDesc": "Настройка колеса удачи и призов", "tariffsDesc": "Управление тарифными планами", - "serversDesc": "Настройка VPN серверов" + "serversDesc": "Настройка VPN серверов", + "banSystemDesc": "Мониторинг банов и нарушений" }, "wheel": { "title": "Настройки колеса удачи", @@ -744,6 +746,173 @@ "noTariffs": "Нет тарифов" } }, + "banSystem": { + "title": "Мониторинг банов", + "subtitle": "Управление системой банов BedolagaBan", + "notConfigured": "Ban система не настроена", + "notConfiguredDesc": "Укажите BAN_SYSTEM_API_URL и BAN_SYSTEM_API_TOKEN в конфигурации", + "loadError": "Не удалось загрузить данные", + "tabs": { + "dashboard": "Статистика", + "users": "Пользователи", + "punishments": "Баны", + "nodes": "Ноды", + "agents": "Агенты", + "violations": "Нарушения" + }, + "dashboard": { + "totalUsers": "Всего пользователей", + "activeUsers": "Активных пользователей", + "usersOverLimit": "Превысили лимит", + "totalRequests": "Всего запросов", + "totalPunishments": "Всего наказаний", + "activePunishments": "Активных банов", + "nodesOnline": "Нод онлайн", + "agentsOnline": "Агентов онлайн", + "panelConnected": "Панель подключена", + "panelDisconnected": "Панель отключена", + "uptime": "Аптайм" + }, + "users": { + "title": "Пользователи", + "searchPlaceholder": "Поиск по email...", + "email": "Email", + "uniqueIps": "Уникальных IP", + "requests": "Запросов", + "limit": "Лимит", + "status": "Статус", + "lastSeen": "Последняя активность", + "overLimit": "Превышен лимит", + "normal": "Норма", + "noLimit": "Без лимита", + "noUsers": "Пользователи не найдены", + "viewDetails": "Подробнее", + "filter": { + "all": "Все", + "overLimit": "Превысили лимит", + "normal": "В норме" + } + }, + "userDetail": { + "title": "Детали пользователя", + "email": "Email", + "uniqueIps": "Уникальных IP", + "totalRequests": "Всего запросов", + "limit": "Лимит устройств", + "status": "Статус", + "networkType": "Тип сети", + "ipHistory": "История IP", + "recentRequests": "Последние запросы", + "ip": "IP адрес", + "firstSeen": "Первое подключение", + "lastSeen": "Последнее подключение", + "node": "Нода", + "requestCount": "Запросов", + "country": "Страна", + "city": "Город", + "timestamp": "Время", + "sourceIp": "IP источника", + "destination": "Назначение", + "port": "Порт", + "protocol": "Протокол", + "action": "Действие", + "noIps": "Нет данных об IP", + "noRequests": "Нет данных о запросах", + "ban": "Забанить", + "close": "Закрыть" + }, + "punishments": { + "title": "Активные баны", + "username": "Пользователь", + "reason": "Причина", + "punishedAt": "Забанен", + "enableAt": "Разбан в", + "ipCount": "IP", + "limit": "Лимит", + "node": "Нода", + "actions": "Действия", + "unban": "Разбанить", + "noPunishments": "Активных банов нет", + "unbanConfirm": "Разбанить пользователя {{username}}?", + "unbanSuccess": "Пользователь разбанен", + "unbanError": "Ошибка разбана", + "history": "История банов", + "searchHistory": "Поиск истории...", + "noHistory": "История не найдена" + }, + "banModal": { + "title": "Забанить пользователя", + "username": "Username", + "usernamePlaceholder": "Введите username", + "duration": "Длительность (минуты)", + "durationPlaceholder": "Введите длительность в минутах", + "reason": "Причина", + "reasonPlaceholder": "Введите причину бана (опционально)", + "cancel": "Отмена", + "ban": "Забанить", + "success": "Пользователь забанен", + "error": "Ошибка бана" + }, + "nodes": { + "title": "Ноды", + "name": "Название", + "address": "Адрес", + "status": "Статус", + "lastSeen": "Последняя активность", + "usersCount": "Пользователей", + "online": "Онлайн", + "offline": "Оффлайн", + "noNodes": "Ноды не найдены", + "total": "Всего", + "onlineCount": "Онлайн" + }, + "agents": { + "title": "Агенты", + "nodeName": "Нода", + "status": "Статус", + "health": "Здоровье", + "sent": "Отправлено", + "dropped": "Отброшено", + "batches": "Пакетов", + "reconnects": "Переподключений", + "failures": "Ошибок", + "queue": "Очередь", + "dedup": "Дедупликация", + "filter": "Фильтрация", + "lastReport": "Последний отчёт", + "online": "Онлайн", + "offline": "Оффлайн", + "healthy": "Здоров", + "warning": "Предупреждение", + "critical": "Критический", + "noAgents": "Агенты не найдены", + "summary": { + "title": "Сводка", + "totalAgents": "Всего агентов", + "onlineAgents": "Онлайн", + "totalSent": "Отправлено", + "totalDropped": "Отброшено", + "avgQueueSize": "Средний размер очереди", + "healthyCount": "Здоровых", + "warningCount": "С предупреждениями", + "criticalCount": "Критических" + } + }, + "violations": { + "title": "Нарушения трафика", + "username": "Пользователь", + "email": "Email", + "type": "Тип", + "description": "Описание", + "bytesUsed": "Использовано", + "bytesLimit": "Лимит", + "detectedAt": "Обнаружено", + "resolved": "Решено", + "noViolations": "Нарушений не обнаружено", + "yes": "Да", + "no": "Нет" + } + }, "profile": { "title": "Профиль", "accountInfo": "Информация об аккаунте", diff --git a/src/pages/AdminBanSystem.tsx b/src/pages/AdminBanSystem.tsx new file mode 100644 index 0000000..05869e4 --- /dev/null +++ b/src/pages/AdminBanSystem.tsx @@ -0,0 +1,722 @@ +import { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { + banSystemApi, + type BanSystemStatus, + type BanSystemStats, + type BanUsersListResponse, + type BanUserDetailResponse, + type BanPunishmentsListResponse, + type BanNodesListResponse, + type BanAgentsListResponse, + type BanTrafficViolationsResponse, +} from '../api/banSystem' + +// Icons +const ShieldIcon = () => ( + + + +) + +const UsersIcon = () => ( + + + +) + +const BanIcon = () => ( + + + +) + +const ServerIcon = () => ( + + + +) + +const AgentIcon = () => ( + + + +) + +const WarningIcon = () => ( + + + +) + +const RefreshIcon = () => ( + + + +) + +const ChartIcon = () => ( + + + +) + +const SearchIcon = () => ( + + + +) + +type TabType = 'dashboard' | 'users' | 'punishments' | 'nodes' | 'agents' | 'violations' + +interface StatCardProps { + title: string + value: string | number + subtitle?: string + icon: React.ReactNode + color: 'accent' | 'success' | 'warning' | 'error' | 'info' +} + +function StatCard({ title, value, subtitle, icon, color }: StatCardProps) { + const colorClasses = { + accent: 'bg-accent-500/20 text-accent-400', + success: 'bg-success-500/20 text-success-400', + warning: 'bg-warning-500/20 text-warning-400', + error: 'bg-error-500/20 text-error-400', + info: 'bg-info-500/20 text-info-400', + } + + return ( +
+
+
+ {icon} +
+
+
{value}
+
{title}
+ {subtitle &&
{subtitle}
} +
+ ) +} + +export default function AdminBanSystem() { + const { t } = useTranslation() + const [activeTab, setActiveTab] = useState('dashboard') + const [status, setStatus] = useState(null) + const [stats, setStats] = useState(null) + const [users, setUsers] = useState(null) + const [selectedUser, setSelectedUser] = useState(null) + const [punishments, setPunishments] = useState(null) + const [nodes, setNodes] = useState(null) + const [agents, setAgents] = useState(null) + const [violations, setViolations] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + const [actionLoading, setActionLoading] = useState(null) + + useEffect(() => { + loadStatus() + }, []) + + useEffect(() => { + if (status?.enabled && status?.configured) { + loadTabData(activeTab) + } + }, [activeTab, status]) + + const loadStatus = async () => { + try { + setLoading(true) + const data = await banSystemApi.getStatus() + setStatus(data) + if (!data.enabled || !data.configured) { + setError(t('banSystem.notConfigured')) + } + } catch { + setError(t('banSystem.loadError')) + } finally { + setLoading(false) + } + } + + const loadTabData = async (tab: TabType) => { + try { + setLoading(true) + setError(null) + + switch (tab) { + case 'dashboard': + const statsData = await banSystemApi.getStats() + setStats(statsData) + break + case 'users': + const usersData = await banSystemApi.getUsers({ limit: 50 }) + setUsers(usersData) + break + case 'punishments': + const punishmentsData = await banSystemApi.getPunishments() + setPunishments(punishmentsData) + break + case 'nodes': + const nodesData = await banSystemApi.getNodes() + setNodes(nodesData) + break + case 'agents': + const agentsData = await banSystemApi.getAgents() + setAgents(agentsData) + break + case 'violations': + const violationsData = await banSystemApi.getTrafficViolations() + setViolations(violationsData) + break + } + } catch { + setError(t('banSystem.loadError')) + } finally { + setLoading(false) + } + } + + const handleSearch = async () => { + if (!searchQuery.trim()) { + loadTabData('users') + return + } + try { + setLoading(true) + const data = await banSystemApi.searchUsers(searchQuery) + setUsers(data) + } catch { + setError(t('banSystem.loadError')) + } finally { + setLoading(false) + } + } + + const handleViewUser = async (email: string) => { + try { + setActionLoading(email) + const data = await banSystemApi.getUser(email) + setSelectedUser(data) + } catch { + setError(t('banSystem.loadError')) + } finally { + setActionLoading(null) + } + } + + const handleUnban = async (userId: string) => { + try { + setActionLoading(userId) + await banSystemApi.unbanUser(userId) + loadTabData('punishments') + } catch { + setError(t('banSystem.loadError')) + } finally { + setActionLoading(null) + } + } + + const formatUptime = (seconds: number | null) => { + if (!seconds) return '-' + const hours = Math.floor(seconds / 3600) + const minutes = Math.floor((seconds % 3600) / 60) + if (hours > 24) { + const days = Math.floor(hours / 24) + return `${days}d ${hours % 24}h` + } + return `${hours}h ${minutes}m` + } + + const formatDate = (dateStr: string | null) => { + if (!dateStr) return '-' + return new Date(dateStr).toLocaleString() + } + + const tabs = [ + { id: 'dashboard' as TabType, label: t('banSystem.tabs.dashboard'), icon: }, + { id: 'users' as TabType, label: t('banSystem.tabs.users'), icon: }, + { id: 'punishments' as TabType, label: t('banSystem.tabs.punishments'), icon: }, + { id: 'nodes' as TabType, label: t('banSystem.tabs.nodes'), icon: }, + { id: 'agents' as TabType, label: t('banSystem.tabs.agents'), icon: }, + { id: 'violations' as TabType, label: t('banSystem.tabs.violations'), icon: }, + ] + + if (loading && !status) { + return ( +
+
+
+ ) + } + + if (error && !status?.enabled) { + return ( +
+
{error}
+

{t('banSystem.configureHint')}

+
+ ) + } + + return ( +
+ {/* Header */} +
+
+
+ +
+
+

{t('banSystem.title')}

+

{t('banSystem.subtitle')}

+
+
+ +
+ + {/* Tabs */} +
+ {tabs.map((tab) => ( + + ))} +
+ + {/* Content */} + {loading ? ( +
+
+
+ ) : error ? ( +
{error}
+ ) : ( + <> + {/* Dashboard Tab */} + {activeTab === 'dashboard' && stats && ( +
+ } + color="success" + /> + } + color="warning" + /> + } + color="error" + /> + } + color="accent" + /> + } + color="info" + /> + } + color="accent" + /> + } + color={stats.panel_connected ? 'success' : 'error'} + /> + } + color="info" + /> +
+ )} + + {/* Users Tab */} + {activeTab === 'users' && ( +
+ {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + placeholder={t('banSystem.users.searchPlaceholder')} + className="w-full pl-10 pr-4 py-2 bg-dark-800 border border-dark-700 rounded-lg text-dark-100 placeholder-dark-500 focus:outline-none focus:border-accent-500" + /> +
+ +
+ + {/* Users Table */} +
+ + + + + + + + + + + + + {users?.users.map((user) => ( + + + + + + + + + ))} + +
{t('banSystem.users.email')}{t('banSystem.users.ipCount')}{t('banSystem.users.limit')}{t('banSystem.users.status')}{t('banSystem.users.bans')}{t('common.actions')}
{user.email}{user.unique_ip_count}{user.limit ?? '-'} + + {user.is_over_limit ? t('banSystem.users.overLimit') : t('banSystem.users.ok')} + + {user.blocked_count} + +
+ {(!users?.users || users.users.length === 0) && ( +
{t('common.noData')}
+ )} +
+
+ )} + + {/* Punishments Tab */} + {activeTab === 'punishments' && ( +
+ + + + + + + + + + + + + + {punishments?.punishments.map((p) => ( + + + + + + + + + + ))} + +
{t('banSystem.punishments.user')}{t('banSystem.punishments.reason')}{t('banSystem.punishments.ipCount')}{t('banSystem.punishments.limit')}{t('banSystem.punishments.bannedAt')}{t('banSystem.punishments.enableAt')}{t('common.actions')}
+
{p.username}
+
{p.user_id}
+
{p.reason || '-'}{p.ip_count}{p.limit}{formatDate(p.punished_at)}{formatDate(p.enable_at)} + +
+ {(!punishments?.punishments || punishments.punishments.length === 0) && ( +
{t('banSystem.punishments.noBans')}
+ )} +
+ )} + + {/* Nodes Tab */} + {activeTab === 'nodes' && ( +
+ {nodes?.nodes.map((node) => ( +
+
+
+
+
{node.name}
+
{node.address || '-'}
+
+
+
+
+
{t('banSystem.nodes.status')}
+
+ {node.is_connected ? t('banSystem.nodes.online') : t('banSystem.nodes.offline')} +
+
+
+
{t('banSystem.nodes.users')}
+
{node.users_count}
+
+
+
+ ))} + {(!nodes?.nodes || nodes.nodes.length === 0) && ( +
{t('banSystem.nodes.noNodes')}
+ )} +
+ )} + + {/* Agents Tab */} + {activeTab === 'agents' && ( +
+ {/* Summary */} + {agents?.summary && ( +
+ } + color="success" + /> + } + color="accent" + /> + } + color="warning" + /> + } + color="info" + /> +
+ )} + + {/* Agents List */} +
+ + + + + + + + + + + + + {agents?.agents.map((agent) => ( + + + + + + + + + ))} + +
{t('banSystem.agents.node')}{t('banSystem.agents.status')}{t('banSystem.agents.health')}{t('banSystem.agents.sent')}{t('banSystem.agents.dropped')}{t('banSystem.agents.queue')}
{agent.node_name} + + {agent.is_online ? t('banSystem.agents.online') : t('banSystem.agents.offline')} + + + + {agent.health} + + {agent.sent_total.toLocaleString()}{agent.dropped_total.toLocaleString()}{agent.queue_size}/{agent.queue_max}
+ {(!agents?.agents || agents.agents.length === 0) && ( +
{t('banSystem.agents.noAgents')}
+ )} +
+
+ )} + + {/* Violations Tab */} + {activeTab === 'violations' && ( +
+ + + + + + + + + + + + {violations?.violations.map((v, idx) => ( + + + + + + + + ))} + +
{t('banSystem.violations.user')}{t('banSystem.violations.type')}{t('banSystem.violations.description')}{t('banSystem.violations.detectedAt')}{t('banSystem.violations.status')}
+
{v.username}
+
{v.email || '-'}
+
{v.violation_type}{v.description || '-'}{formatDate(v.detected_at)} + + {v.resolved ? t('banSystem.violations.resolved') : t('banSystem.violations.active')} + +
+ {(!violations?.violations || violations.violations.length === 0) && ( +
{t('banSystem.violations.noViolations')}
+ )} +
+ )} + + )} + + {/* User Detail Modal */} + {selectedUser && ( +
setSelectedUser(null)}> +
e.stopPropagation()}> +
+

{t('banSystem.userDetail.title')}

+ +
+
+ {/* User Info */} +
+
+
{t('banSystem.users.email')}
+
{selectedUser.email}
+
+
+
{t('banSystem.users.limit')}
+
{selectedUser.limit ?? '-'}
+
+
+
{t('banSystem.users.ipCount')}
+
{selectedUser.unique_ip_count}
+
+
+
{t('banSystem.users.networkType')}
+
{selectedUser.network_type || '-'}
+
+
+ + {/* IP History */} +
+

{t('banSystem.userDetail.ipHistory')}

+
+ + + + + + + + + + + {selectedUser.ips.map((ip, idx) => ( + + + + + + + ))} + +
{t('banSystem.userDetail.ip')}{t('banSystem.userDetail.country')}{t('banSystem.userDetail.node')}{t('banSystem.userDetail.requests')}
{ip.ip}{ip.country_name || ip.country_code || '-'}{ip.node || '-'}{ip.request_count}
+ {selectedUser.ips.length === 0 && ( +
{t('common.noData')}
+ )} +
+
+
+
+
+ )} +
+ ) +} diff --git a/src/pages/AdminPanel.tsx b/src/pages/AdminPanel.tsx index 98fba07..4394ca1 100644 --- a/src/pages/AdminPanel.tsx +++ b/src/pages/AdminPanel.tsx @@ -51,6 +51,12 @@ const ChartIcon = () => ( ) +const BanSystemIcon = () => ( + + + +) + interface AdminCardProps { to: string icon: React.ReactNode @@ -129,6 +135,13 @@ export default function AdminPanel() { description: t('admin.panel.serversDesc'), color: 'purple' }, + { + to: '/admin/ban-system', + icon: , + title: t('admin.nav.banSystem'), + description: t('admin.panel.banSystemDesc'), + color: 'error' + }, ] return (