mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -1,5 +1,3 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
@@ -11,16 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
@@ -24,39 +24,10 @@ jobs:
|
||||
run: npm ci
|
||||
|
||||
- name: Run ESLint
|
||||
@@ -59,4 +48,4 @@ jobs:
|
||||
with:
|
||||
name: dist
|
||||
path: dist/
|
||||
retention-days: 7
|
||||
retention-days: 7
|
||||
53
.github/workflows/docker-hub.yml
vendored
53
.github/workflows/docker-hub.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Docker Hub Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build-and-push-dockerhub:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/bedolaga-cabinet
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=sha
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
|
||||
- name: Build and push to Docker Hub
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
build-args: |
|
||||
VITE_API_URL=/api
|
||||
VITE_TELEGRAM_BOT_USERNAME=
|
||||
VITE_APP_NAME=Cabinet
|
||||
VITE_APP_LOGO=V
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -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'
|
||||
import AdminBroadcasts from './pages/AdminBroadcasts'
|
||||
import AdminPromocodes from './pages/AdminPromocodes'
|
||||
import AdminCampaigns from './pages/AdminCampaigns'
|
||||
@@ -224,6 +225,14 @@ function App() {
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/ban-system"
|
||||
element={
|
||||
<AdminRoute>
|
||||
<AdminBanSystem />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/broadcasts"
|
||||
element={
|
||||
|
||||
445
src/api/banSystem.ts
Normal file
445
src/api/banSystem.ts
Normal file
@@ -0,0 +1,445 @@
|
||||
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<string, unknown> | 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
|
||||
}
|
||||
|
||||
export interface BanTrafficTopItem {
|
||||
username: string
|
||||
bytes_total: number
|
||||
bytes_limit: number | null
|
||||
over_limit: boolean
|
||||
}
|
||||
|
||||
export interface BanTrafficResponse {
|
||||
enabled: boolean
|
||||
stats: Record<string, unknown> | null
|
||||
top_users: BanTrafficTopItem[]
|
||||
recent_violations: BanTrafficViolationItem[]
|
||||
}
|
||||
|
||||
// === Settings Types ===
|
||||
|
||||
export interface BanSettingDefinition {
|
||||
key: string
|
||||
value: unknown
|
||||
type: string
|
||||
min_value: number | null
|
||||
max_value: number | null
|
||||
editable: boolean
|
||||
description: string | null
|
||||
category: string | null
|
||||
}
|
||||
|
||||
export interface BanSettingsResponse {
|
||||
settings: BanSettingDefinition[]
|
||||
}
|
||||
|
||||
export interface BanWhitelistRequest {
|
||||
username: string
|
||||
}
|
||||
|
||||
// === Report Types ===
|
||||
|
||||
export interface BanReportTopViolator {
|
||||
username: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export interface BanReportResponse {
|
||||
period_hours: number
|
||||
current_users: number
|
||||
current_ips: number
|
||||
punishment_stats: Record<string, unknown> | null
|
||||
top_violators: BanReportTopViolator[]
|
||||
}
|
||||
|
||||
// === Health Types ===
|
||||
|
||||
export interface BanHealthComponent {
|
||||
name: string
|
||||
status: string
|
||||
message: string | null
|
||||
details: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface BanHealthResponse {
|
||||
status: string
|
||||
uptime: number | null
|
||||
components: BanHealthComponent[]
|
||||
}
|
||||
|
||||
export interface BanHealthDetailedResponse {
|
||||
status: string
|
||||
uptime: number | null
|
||||
components: Record<string, unknown>
|
||||
}
|
||||
|
||||
// === Agent History Types ===
|
||||
|
||||
export interface BanAgentHistoryItem {
|
||||
timestamp: string
|
||||
sent_total: number
|
||||
dropped_total: number
|
||||
queue_size: number
|
||||
batches_total: number
|
||||
}
|
||||
|
||||
export interface BanAgentHistoryResponse {
|
||||
node: string
|
||||
hours: number
|
||||
records: number
|
||||
delta: Record<string, unknown> | null
|
||||
first: Record<string, unknown> | null
|
||||
last: Record<string, unknown> | null
|
||||
history: BanAgentHistoryItem[]
|
||||
}
|
||||
|
||||
// === API ===
|
||||
|
||||
export const banSystemApi = {
|
||||
// Status
|
||||
getStatus: async (): Promise<BanSystemStatus> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/status')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Stats
|
||||
getStats: async (): Promise<BanSystemStats> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/stats')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Users
|
||||
getUsers: async (params: {
|
||||
offset?: number
|
||||
limit?: number
|
||||
status?: string
|
||||
} = {}): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/users', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
getUsersOverLimit: async (limit: number = 50): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/users/over-limit', {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
searchUsers: async (query: string): Promise<BanUsersListResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/users/search/${encodeURIComponent(query)}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
getUser: async (email: string): Promise<BanUserDetailResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Punishments
|
||||
getPunishments: async (): Promise<BanPunishmentsListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/punishments')
|
||||
return response.data
|
||||
},
|
||||
|
||||
unbanUser: async (userId: string): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/punishments/${userId}/unban`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
banUser: async (data: BanUserRequest): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/ban', data)
|
||||
return response.data
|
||||
},
|
||||
|
||||
getPunishmentHistory: async (query: string, limit: number = 20): Promise<BanHistoryResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/history/${encodeURIComponent(query)}`, {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Nodes
|
||||
getNodes: async (): Promise<BanNodesListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/nodes')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Agents
|
||||
getAgents: async (params: {
|
||||
search?: string
|
||||
health?: string
|
||||
status?: string
|
||||
} = {}): Promise<BanAgentsListResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params })
|
||||
return response.data
|
||||
},
|
||||
|
||||
getAgentsSummary: async (): Promise<BanAgentsSummary> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/agents/summary')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Traffic violations
|
||||
getTrafficViolations: async (limit: number = 50): Promise<BanTrafficViolationsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic/violations', {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Full Traffic
|
||||
getTraffic: async (): Promise<BanTrafficResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic')
|
||||
return response.data
|
||||
},
|
||||
|
||||
getTrafficTop: async (limit: number = 20): Promise<BanTrafficTopItem[]> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/traffic/top', {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Settings
|
||||
getSettings: async (): Promise<BanSettingsResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/settings')
|
||||
return response.data
|
||||
},
|
||||
|
||||
getSetting: async (key: string): Promise<BanSettingDefinition> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/settings/${key}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
setSetting: async (key: string, value: string): Promise<BanSettingDefinition> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}`, null, {
|
||||
params: { value }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
toggleSetting: async (key: string): Promise<BanSettingDefinition> => {
|
||||
const response = await apiClient.post(`/cabinet/admin/ban-system/settings/${key}/toggle`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Whitelist
|
||||
whitelistAdd: async (username: string): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/add', { username })
|
||||
return response.data
|
||||
},
|
||||
|
||||
whitelistRemove: async (username: string): Promise<UnbanResponse> => {
|
||||
const response = await apiClient.post('/cabinet/admin/ban-system/settings/whitelist/remove', { username })
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Reports
|
||||
getReport: async (hours: number = 24): Promise<BanReportResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/report', {
|
||||
params: { hours }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Health
|
||||
getHealth: async (): Promise<BanHealthResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/health')
|
||||
return response.data
|
||||
},
|
||||
|
||||
getHealthDetailed: async (): Promise<BanHealthDetailedResponse> => {
|
||||
const response = await apiClient.get('/cabinet/admin/ban-system/health/detailed')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// Agent History
|
||||
getAgentHistory: async (nodeName: string, hours: number = 24): Promise<BanAgentHistoryResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/agents/${encodeURIComponent(nodeName)}/history`, {
|
||||
params: { hours }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
|
||||
// User Punishment History
|
||||
getUserHistory: async (email: string, limit: number = 20): Promise<BanHistoryResponse> => {
|
||||
const response = await apiClient.get(`/cabinet/admin/ban-system/users/${encodeURIComponent(email)}/history`, {
|
||||
params: { limit }
|
||||
})
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
@@ -15,10 +15,12 @@
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"or": "or",
|
||||
"add": "Add",
|
||||
"and": "and",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"currency": "$"
|
||||
"currency": "$",
|
||||
"refresh": "Refresh"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
@@ -449,6 +451,7 @@
|
||||
"wheel": "Wheel",
|
||||
"tariffs": "Tariffs",
|
||||
"servers": "Servers",
|
||||
"banSystem": "Ban Monitoring",
|
||||
"broadcasts": "Broadcasts",
|
||||
"users": "Users",
|
||||
"payments": "Payments",
|
||||
@@ -464,6 +467,7 @@
|
||||
"wheelDesc": "Configure fortune wheel and prizes",
|
||||
"tariffsDesc": "Manage tariff plans",
|
||||
"serversDesc": "Configure VPN servers",
|
||||
"banSystemDesc": "Ban monitoring and violations",
|
||||
"broadcastsDesc": "Mass messaging to users",
|
||||
"usersDesc": "Manage bot users",
|
||||
"paymentsDesc": "Payment verification",
|
||||
@@ -863,6 +867,283 @@
|
||||
"noTariffs": "No tariffs"
|
||||
}
|
||||
},
|
||||
"banSystem": {
|
||||
"title": "Ban Monitoring",
|
||||
"subtitle": "BedolagaBan system management",
|
||||
"notConfigured": "Ban system is not configured",
|
||||
"configureHint": "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",
|
||||
"traffic": "Traffic",
|
||||
"reports": "Reports",
|
||||
"settings": "Settings",
|
||||
"health": "Health"
|
||||
},
|
||||
"stats": {
|
||||
"activeUsers": "Active Users",
|
||||
"total": "Total",
|
||||
"usersOverLimit": "Over Limit",
|
||||
"activeBans": "Active Bans",
|
||||
"nodesOnline": "Nodes Online",
|
||||
"agentsOnline": "Agents Online",
|
||||
"totalRequests": "Total Requests",
|
||||
"panelStatus": "Panel Status",
|
||||
"connected": "Connected",
|
||||
"disconnected": "Disconnected",
|
||||
"uptime": "Uptime"
|
||||
},
|
||||
"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",
|
||||
"ipCount": "IPs",
|
||||
"requests": "Requests",
|
||||
"limit": "Limit",
|
||||
"status": "Status",
|
||||
"bans": "Bans",
|
||||
"lastSeen": "Last Seen",
|
||||
"overLimit": "Over Limit",
|
||||
"ok": "OK",
|
||||
"normal": "Normal",
|
||||
"noLimit": "No Limit",
|
||||
"noUsers": "No users found",
|
||||
"viewDetails": "View Details",
|
||||
"networkType": "Network Type",
|
||||
"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",
|
||||
"requests": "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",
|
||||
"user": "User",
|
||||
"username": "User",
|
||||
"reason": "Reason",
|
||||
"punishedAt": "Banned At",
|
||||
"bannedAt": "Banned At",
|
||||
"enableAt": "Unban At",
|
||||
"ipCount": "IPs",
|
||||
"limit": "Limit",
|
||||
"node": "Node",
|
||||
"actions": "Actions",
|
||||
"unban": "Unban",
|
||||
"noPunishments": "No active bans",
|
||||
"noBans": "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",
|
||||
"users": "Users",
|
||||
"online": "Online",
|
||||
"offline": "Offline",
|
||||
"noNodes": "No nodes found",
|
||||
"total": "Total",
|
||||
"onlineCount": "Online"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Agents",
|
||||
"nodeName": "Node",
|
||||
"node": "Node",
|
||||
"status": "Status",
|
||||
"health": "Health",
|
||||
"sent": "Sent",
|
||||
"dropped": "Dropped",
|
||||
"totalSent": "Total Sent",
|
||||
"totalDropped": "Total 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",
|
||||
"user": "User",
|
||||
"username": "User",
|
||||
"email": "Email",
|
||||
"type": "Type",
|
||||
"description": "Description",
|
||||
"bytesUsed": "Used",
|
||||
"bytesLimit": "Limit",
|
||||
"detectedAt": "Detected At",
|
||||
"status": "Status",
|
||||
"resolved": "Resolved",
|
||||
"active": "Active",
|
||||
"noViolations": "No violations found",
|
||||
"yes": "Yes",
|
||||
"no": "No"
|
||||
},
|
||||
"traffic": {
|
||||
"title": "Traffic Statistics",
|
||||
"enabled": "Traffic Monitoring",
|
||||
"topUsers": "Top by Traffic",
|
||||
"username": "User",
|
||||
"bytesTotal": "Total",
|
||||
"bytesLimit": "Limit",
|
||||
"status": "Status",
|
||||
"overLimit": "Over Limit",
|
||||
"ok": "OK",
|
||||
"recentViolations": "Recent Violations"
|
||||
},
|
||||
"reports": {
|
||||
"title": "Period Reports",
|
||||
"period": "Period",
|
||||
"currentUsers": "Active Users",
|
||||
"currentIps": "Unique IPs",
|
||||
"topViolators": "Top Violators",
|
||||
"username": "User",
|
||||
"count": "Violations"
|
||||
},
|
||||
"settings": {
|
||||
"title": "System Settings",
|
||||
"general": "General",
|
||||
"limits": "Limits",
|
||||
"notifications": "Notifications",
|
||||
"whitelist": "Whitelist",
|
||||
"saved": "Setting saved",
|
||||
"error": "Error saving",
|
||||
"categories": {
|
||||
"general": "General Settings",
|
||||
"punishment": "Punishments",
|
||||
"progressive_bans": "Progressive Bans",
|
||||
"traffic": "Traffic Monitoring",
|
||||
"network": "Network Detection",
|
||||
"notifications": "Notifications",
|
||||
"rate_limit": "Rate Limits"
|
||||
},
|
||||
"punishment_enabled": "Enable auto-ban",
|
||||
"punishment_minutes": "Ban duration (minutes)",
|
||||
"ip_window_seconds": "IP counting window (seconds)",
|
||||
"notify_on_punishment": "Notify on bans",
|
||||
"notify_on_node_status": "Notify on node status",
|
||||
"daily_report_enabled": "Daily report",
|
||||
"daily_report_hour": "Report hour",
|
||||
"rate_limit_max": "Max requests",
|
||||
"rate_limit_window": "Rate limit window (sec)",
|
||||
"heartbeat_timeout": "Heartbeat timeout (sec)",
|
||||
"progressive_bans_enabled": "Progressive bans",
|
||||
"progressive_ban_1": "1st ban (minutes)",
|
||||
"progressive_ban_2": "2nd ban (minutes)",
|
||||
"progressive_ban_3": "3rd ban (minutes)",
|
||||
"progressive_ban_window_hours": "Reset window (hours)",
|
||||
"traffic_monitor_enabled": "Traffic monitoring",
|
||||
"traffic_limit_gb": "Traffic limit (GB)",
|
||||
"traffic_window_minutes": "Check window (min)",
|
||||
"traffic_check_interval": "Check interval (min)",
|
||||
"traffic_ban_minutes": "Traffic ban (min)",
|
||||
"network_detection_enabled": "Network type detection",
|
||||
"network_detection_nodes": "Detection nodes",
|
||||
"network_detection_monitor_all": "Monitor all nodes",
|
||||
"network_detection_collect_all": "Collect from all nodes",
|
||||
"network_notify_mobile": "Notify on mobile",
|
||||
"network_block_mobile": "Block mobile",
|
||||
"network_block_mobile_minutes": "Block mobile (min)",
|
||||
"network_notify_wifi": "Notify on WiFi",
|
||||
"network_block_wifi": "Block WiFi",
|
||||
"network_block_wifi_minutes": "Block WiFi (min)"
|
||||
},
|
||||
"health": {
|
||||
"title": "System Health",
|
||||
"systemStatus": "System Status",
|
||||
"healthy": "Healthy",
|
||||
"degraded": "Degraded",
|
||||
"unhealthy": "Unhealthy",
|
||||
"components": "Components",
|
||||
"uptime": "Uptime"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile",
|
||||
"accountInfo": "Account Information",
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
"yes": "Да",
|
||||
"no": "Нет",
|
||||
"or": "или",
|
||||
"add": "Добавить",
|
||||
"and": "и",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
"currency": "₽"
|
||||
"currency": "₽",
|
||||
"refresh": "Обновить"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Главная",
|
||||
@@ -449,6 +451,7 @@
|
||||
"wheel": "Колесо",
|
||||
"tariffs": "Тарифы",
|
||||
"servers": "Серверы",
|
||||
"banSystem": "Мониторинг банов",
|
||||
"broadcasts": "Рассылки",
|
||||
"users": "Пользователи",
|
||||
"payments": "Платежи",
|
||||
@@ -464,6 +467,7 @@
|
||||
"wheelDesc": "Настройка колеса удачи и призов",
|
||||
"tariffsDesc": "Управление тарифными планами",
|
||||
"serversDesc": "Настройка VPN серверов",
|
||||
"banSystemDesc": "Мониторинг банов и нарушений",
|
||||
"broadcastsDesc": "Массовая отправка сообщений",
|
||||
"usersDesc": "Управление пользователями бота",
|
||||
"paymentsDesc": "Проверка платежей",
|
||||
@@ -863,6 +867,283 @@
|
||||
"noTariffs": "Нет тарифов"
|
||||
}
|
||||
},
|
||||
"banSystem": {
|
||||
"title": "Мониторинг банов",
|
||||
"subtitle": "Управление системой банов BedolagaBan",
|
||||
"notConfigured": "Ban система не настроена",
|
||||
"configureHint": "Укажите BAN_SYSTEM_API_URL и BAN_SYSTEM_API_TOKEN в конфигурации",
|
||||
"loadError": "Не удалось загрузить данные",
|
||||
"tabs": {
|
||||
"dashboard": "Статистика",
|
||||
"users": "Пользователи",
|
||||
"punishments": "Баны",
|
||||
"nodes": "Ноды",
|
||||
"agents": "Агенты",
|
||||
"violations": "Нарушения",
|
||||
"traffic": "Трафик",
|
||||
"reports": "Отчёты",
|
||||
"settings": "Настройки",
|
||||
"health": "Здоровье"
|
||||
},
|
||||
"stats": {
|
||||
"activeUsers": "Активных пользователей",
|
||||
"total": "Всего",
|
||||
"usersOverLimit": "Превысили лимит",
|
||||
"activeBans": "Активных банов",
|
||||
"nodesOnline": "Нод онлайн",
|
||||
"agentsOnline": "Агентов онлайн",
|
||||
"totalRequests": "Всего запросов",
|
||||
"panelStatus": "Статус панели",
|
||||
"connected": "Подключена",
|
||||
"disconnected": "Отключена",
|
||||
"uptime": "Аптайм"
|
||||
},
|
||||
"dashboard": {
|
||||
"totalUsers": "Всего пользователей",
|
||||
"activeUsers": "Активных пользователей",
|
||||
"usersOverLimit": "Превысили лимит",
|
||||
"totalRequests": "Всего запросов",
|
||||
"totalPunishments": "Всего наказаний",
|
||||
"activePunishments": "Активных банов",
|
||||
"nodesOnline": "Нод онлайн",
|
||||
"agentsOnline": "Агентов онлайн",
|
||||
"panelConnected": "Панель подключена",
|
||||
"panelDisconnected": "Панель отключена",
|
||||
"uptime": "Аптайм"
|
||||
},
|
||||
"users": {
|
||||
"title": "Пользователи",
|
||||
"searchPlaceholder": "Поиск по email...",
|
||||
"email": "Email",
|
||||
"uniqueIps": "Уникальных IP",
|
||||
"ipCount": "IP",
|
||||
"requests": "Запросов",
|
||||
"limit": "Лимит",
|
||||
"status": "Статус",
|
||||
"bans": "Баны",
|
||||
"lastSeen": "Последняя активность",
|
||||
"overLimit": "Превышен лимит",
|
||||
"ok": "OK",
|
||||
"normal": "Норма",
|
||||
"noLimit": "Без лимита",
|
||||
"noUsers": "Пользователи не найдены",
|
||||
"viewDetails": "Подробнее",
|
||||
"networkType": "Тип сети",
|
||||
"filter": {
|
||||
"all": "Все",
|
||||
"overLimit": "Превысили лимит",
|
||||
"normal": "В норме"
|
||||
}
|
||||
},
|
||||
"userDetail": {
|
||||
"title": "Детали пользователя",
|
||||
"email": "Email",
|
||||
"uniqueIps": "Уникальных IP",
|
||||
"totalRequests": "Всего запросов",
|
||||
"limit": "Лимит устройств",
|
||||
"status": "Статус",
|
||||
"networkType": "Тип сети",
|
||||
"ipHistory": "История IP",
|
||||
"recentRequests": "Последние запросы",
|
||||
"ip": "IP адрес",
|
||||
"firstSeen": "Первое подключение",
|
||||
"lastSeen": "Последнее подключение",
|
||||
"node": "Нода",
|
||||
"requestCount": "Запросов",
|
||||
"requests": "Запросов",
|
||||
"country": "Страна",
|
||||
"city": "Город",
|
||||
"timestamp": "Время",
|
||||
"sourceIp": "IP источника",
|
||||
"destination": "Назначение",
|
||||
"port": "Порт",
|
||||
"protocol": "Протокол",
|
||||
"action": "Действие",
|
||||
"noIps": "Нет данных об IP",
|
||||
"noRequests": "Нет данных о запросах",
|
||||
"ban": "Забанить",
|
||||
"close": "Закрыть"
|
||||
},
|
||||
"punishments": {
|
||||
"title": "Активные баны",
|
||||
"user": "Пользователь",
|
||||
"username": "Пользователь",
|
||||
"reason": "Причина",
|
||||
"punishedAt": "Забанен",
|
||||
"bannedAt": "Забанен",
|
||||
"enableAt": "Разбан в",
|
||||
"ipCount": "IP",
|
||||
"limit": "Лимит",
|
||||
"node": "Нода",
|
||||
"actions": "Действия",
|
||||
"unban": "Разбанить",
|
||||
"noPunishments": "Активных банов нет",
|
||||
"noBans": "Активных банов нет",
|
||||
"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": "Пользователей",
|
||||
"users": "Пользователей",
|
||||
"online": "Онлайн",
|
||||
"offline": "Оффлайн",
|
||||
"noNodes": "Ноды не найдены",
|
||||
"total": "Всего",
|
||||
"onlineCount": "Онлайн"
|
||||
},
|
||||
"agents": {
|
||||
"title": "Агенты",
|
||||
"nodeName": "Нода",
|
||||
"node": "Нода",
|
||||
"status": "Статус",
|
||||
"health": "Здоровье",
|
||||
"sent": "Отправлено",
|
||||
"dropped": "Отброшено",
|
||||
"totalSent": "Всего отправлено",
|
||||
"totalDropped": "Всего отброшено",
|
||||
"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": "Нарушения трафика",
|
||||
"user": "Пользователь",
|
||||
"username": "Пользователь",
|
||||
"email": "Email",
|
||||
"type": "Тип",
|
||||
"description": "Описание",
|
||||
"bytesUsed": "Использовано",
|
||||
"bytesLimit": "Лимит",
|
||||
"detectedAt": "Обнаружено",
|
||||
"status": "Статус",
|
||||
"resolved": "Решено",
|
||||
"active": "Активно",
|
||||
"noViolations": "Нарушений не обнаружено",
|
||||
"yes": "Да",
|
||||
"no": "Нет"
|
||||
},
|
||||
"traffic": {
|
||||
"title": "Статистика трафика",
|
||||
"enabled": "Мониторинг трафика",
|
||||
"topUsers": "Топ по трафику",
|
||||
"username": "Пользователь",
|
||||
"bytesTotal": "Всего",
|
||||
"bytesLimit": "Лимит",
|
||||
"status": "Статус",
|
||||
"overLimit": "Превышен",
|
||||
"ok": "OK",
|
||||
"recentViolations": "Последние нарушения"
|
||||
},
|
||||
"reports": {
|
||||
"title": "Отчёты за период",
|
||||
"period": "Период",
|
||||
"currentUsers": "Активных пользователей",
|
||||
"currentIps": "Уникальных IP",
|
||||
"topViolators": "Топ нарушителей",
|
||||
"username": "Пользователь",
|
||||
"count": "Нарушений"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки системы",
|
||||
"general": "Общие",
|
||||
"limits": "Лимиты",
|
||||
"notifications": "Уведомления",
|
||||
"whitelist": "Белый список",
|
||||
"saved": "Настройка сохранена",
|
||||
"error": "Ошибка сохранения",
|
||||
"categories": {
|
||||
"general": "Общие настройки",
|
||||
"punishment": "Наказания",
|
||||
"progressive_bans": "Прогрессивные баны",
|
||||
"traffic": "Мониторинг трафика",
|
||||
"network": "Детекция сети",
|
||||
"notifications": "Уведомления",
|
||||
"rate_limit": "Лимиты запросов"
|
||||
},
|
||||
"punishment_enabled": "Включить автобан",
|
||||
"punishment_minutes": "Длительность бана (минуты)",
|
||||
"ip_window_seconds": "Окно подсчёта IP (секунды)",
|
||||
"notify_on_punishment": "Уведомлять о банах",
|
||||
"notify_on_node_status": "Уведомлять о статусе нод",
|
||||
"daily_report_enabled": "Ежедневный отчёт",
|
||||
"daily_report_hour": "Час отправки отчёта",
|
||||
"rate_limit_max": "Макс. запросов",
|
||||
"rate_limit_window": "Окно лимита (сек)",
|
||||
"heartbeat_timeout": "Таймаут heartbeat (сек)",
|
||||
"progressive_bans_enabled": "Прогрессивные баны",
|
||||
"progressive_ban_1": "1-й бан (минуты)",
|
||||
"progressive_ban_2": "2-й бан (минуты)",
|
||||
"progressive_ban_3": "3-й бан (минуты)",
|
||||
"progressive_ban_window_hours": "Окно сброса (часы)",
|
||||
"traffic_monitor_enabled": "Мониторинг трафика",
|
||||
"traffic_limit_gb": "Лимит трафика (ГБ)",
|
||||
"traffic_window_minutes": "Окно проверки (мин)",
|
||||
"traffic_check_interval": "Интервал проверки (мин)",
|
||||
"traffic_ban_minutes": "Бан за трафик (мин)",
|
||||
"network_detection_enabled": "Детекция типа сети",
|
||||
"network_detection_nodes": "Ноды для детекции",
|
||||
"network_detection_monitor_all": "Мониторить все ноды",
|
||||
"network_detection_collect_all": "Собирать со всех нод",
|
||||
"network_notify_mobile": "Уведомлять о мобильных",
|
||||
"network_block_mobile": "Блокировать мобильные",
|
||||
"network_block_mobile_minutes": "Блок мобильных (мин)",
|
||||
"network_notify_wifi": "Уведомлять о WiFi",
|
||||
"network_block_wifi": "Блокировать WiFi",
|
||||
"network_block_wifi_minutes": "Блок WiFi (мин)"
|
||||
},
|
||||
"health": {
|
||||
"title": "Состояние системы",
|
||||
"systemStatus": "Статус системы",
|
||||
"healthy": "Здорова",
|
||||
"degraded": "Деградация",
|
||||
"unhealthy": "Проблема",
|
||||
"components": "Компоненты",
|
||||
"uptime": "Аптайм"
|
||||
}
|
||||
},
|
||||
"adminUsers": {
|
||||
"title": "Пользователи",
|
||||
"subtitle": "Управление пользователями",
|
||||
|
||||
1199
src/pages/AdminBanSystem.tsx
Normal file
1199
src/pages/AdminBanSystem.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,12 @@ const ChartIcon = ({ className = "w-8 h-8" }: { className?: string }) => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const BanSystemIcon = ({ className = "w-8 h-8" }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const BroadcastIcon = ({ className = "w-8 h-8" }: { className?: string }) => (
|
||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.34 15.84c-.688-.06-1.386-.09-2.09-.09H7.5a4.5 4.5 0 110-9h.75c.704 0 1.402-.03 2.09-.09m0 9.18c.253.962.584 1.892.985 2.783.247.55.06 1.21-.463 1.511l-.657.38c-.551.318-1.26.117-1.527-.461a20.845 20.845 0 01-1.44-4.282m3.102.069a18.03 18.03 0 01-.59-4.59c0-1.586.205-3.124.59-4.59m0 9.18a23.848 23.848 0 018.835 2.535M10.34 6.66a23.847 23.847 0 008.835-2.535m0 0A23.74 23.74 0 0018.795 3m.38 1.125a23.91 23.91 0 011.014 5.395m-1.014 8.855c-.118.38-.245.754-.38 1.125m.38-1.125a23.91 23.91 0 001.014-5.395m0-3.46c.495.413.811 1.035.811 1.73 0 .695-.316 1.317-.811 1.73m0-3.46a24.347 24.347 0 010 3.46" />
|
||||
@@ -272,6 +278,16 @@ export default function AdminPanel() {
|
||||
bgColor: 'bg-indigo-500/20',
|
||||
textColor: 'text-indigo-400'
|
||||
},
|
||||
{
|
||||
to: '/admin/ban-system',
|
||||
icon: <BanSystemIcon />,
|
||||
mobileIcon: <BanSystemIcon className="w-6 h-6" />,
|
||||
title: t('admin.nav.banSystem'),
|
||||
description: t('admin.panel.banSystemDesc'),
|
||||
color: 'error',
|
||||
bgColor: 'bg-red-500/20',
|
||||
textColor: 'text-red-400'
|
||||
},
|
||||
{
|
||||
to: '/admin/payments',
|
||||
icon: <PaymentsIcon />,
|
||||
|
||||
Reference in New Issue
Block a user