mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
43
.github/workflows/ci.yml
vendored
43
.github/workflows/ci.yml
vendored
@@ -1,59 +1,32 @@
|
|||||||
name: CI
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, develop]
|
branches: [main, dev]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, develop]
|
branches: [main, dev]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-and-typecheck:
|
lint-and-build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '20'
|
node-version: '20'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run ESLint
|
- name: Run ESLint
|
||||||
run: npm run lint -- --max-warnings 50
|
run: npm run lint
|
||||||
|
- name: Run TypeScript check
|
||||||
- name: Type check
|
run: npx tsc --noEmit
|
||||||
run: npm run type-check
|
- name: Build
|
||||||
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: lint-and-typecheck
|
|
||||||
|
|
||||||
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
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Build application
|
|
||||||
run: npm run build
|
run: npm run build
|
||||||
env:
|
env:
|
||||||
VITE_API_URL: /api
|
VITE_API_URL: /api
|
||||||
VITE_TELEGRAM_BOT_USERNAME: test_bot
|
VITE_TELEGRAM_BOT_USERNAME: test_bot
|
||||||
VITE_APP_NAME: Cabinet
|
VITE_APP_NAME: Cabinet
|
||||||
VITE_APP_LOGO: V
|
VITE_APP_LOGO: V
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
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
|
|
||||||
22
src/App.tsx
22
src/App.tsx
@@ -1,7 +1,8 @@
|
|||||||
import { Routes, Route, Navigate } from 'react-router-dom'
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
|
||||||
import { useAuthStore } from './store/auth'
|
import { useAuthStore } from './store/auth'
|
||||||
import Layout from './components/layout/Layout'
|
import Layout from './components/layout/Layout'
|
||||||
import PageLoader from './components/common/PageLoader'
|
import PageLoader from './components/common/PageLoader'
|
||||||
|
import { saveReturnUrl } from './utils/token'
|
||||||
import Login from './pages/Login'
|
import Login from './pages/Login'
|
||||||
import TelegramCallback from './pages/TelegramCallback'
|
import TelegramCallback from './pages/TelegramCallback'
|
||||||
import TelegramRedirect from './pages/TelegramRedirect'
|
import TelegramRedirect from './pages/TelegramRedirect'
|
||||||
@@ -25,6 +26,7 @@ import AdminTariffs from './pages/AdminTariffs'
|
|||||||
import AdminServers from './pages/AdminServers'
|
import AdminServers from './pages/AdminServers'
|
||||||
import AdminPanel from './pages/AdminPanel'
|
import AdminPanel from './pages/AdminPanel'
|
||||||
import AdminDashboard from './pages/AdminDashboard'
|
import AdminDashboard from './pages/AdminDashboard'
|
||||||
|
import AdminBanSystem from './pages/AdminBanSystem'
|
||||||
import AdminBroadcasts from './pages/AdminBroadcasts'
|
import AdminBroadcasts from './pages/AdminBroadcasts'
|
||||||
import AdminPromocodes from './pages/AdminPromocodes'
|
import AdminPromocodes from './pages/AdminPromocodes'
|
||||||
import AdminCampaigns from './pages/AdminCampaigns'
|
import AdminCampaigns from './pages/AdminCampaigns'
|
||||||
@@ -35,13 +37,16 @@ import AdminRemnawave from './pages/AdminRemnawave'
|
|||||||
|
|
||||||
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, isLoading } = useAuthStore()
|
const { isAuthenticated, isLoading } = useAuthStore()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <PageLoader variant="dark" />
|
return <PageLoader variant="dark" />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />
|
// Сохраняем текущий URL для возврата после авторизации
|
||||||
|
saveReturnUrl()
|
||||||
|
return <Navigate to="/login" replace state={{ from: location.pathname }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Layout>{children}</Layout>
|
return <Layout>{children}</Layout>
|
||||||
@@ -49,13 +54,16 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
function AdminRoute({ children }: { children: React.ReactNode }) {
|
function AdminRoute({ children }: { children: React.ReactNode }) {
|
||||||
const { isAuthenticated, isLoading, isAdmin } = useAuthStore()
|
const { isAuthenticated, isLoading, isAdmin } = useAuthStore()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <PageLoader variant="light" />
|
return <PageLoader variant="light" />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
return <Navigate to="/login" replace />
|
// Сохраняем текущий URL для возврата после авторизации
|
||||||
|
saveReturnUrl()
|
||||||
|
return <Navigate to="/login" replace state={{ from: location.pathname }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAdmin) {
|
if (!isAdmin) {
|
||||||
@@ -224,6 +232,14 @@ function App() {
|
|||||||
</AdminRoute>
|
</AdminRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/admin/ban-system"
|
||||||
|
element={
|
||||||
|
<AdminRoute>
|
||||||
|
<AdminBanSystem />
|
||||||
|
</AdminRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/admin/broadcasts"
|
path="/admin/broadcasts"
|
||||||
element={
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -259,7 +259,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="sticky top-0 z-50 glass border-b border-dark-800/50">
|
<header className="sticky top-0 z-50 glass border-b border-dark-800/50">
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6">
|
<div className="w-full mx-auto px-4 sm:px-6">
|
||||||
<div className="flex justify-between items-center h-16 lg:h-20">
|
<div className="flex justify-between items-center h-16 lg:h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<Link to="/" className={`flex items-center gap-2.5 flex-shrink-0 ${!appName ? 'lg:mr-4' : ''}`}>
|
<Link to="/" className={`flex items-center gap-2.5 flex-shrink-0 ${!appName ? 'lg:mr-4' : ''}`}>
|
||||||
|
|||||||
@@ -15,10 +15,12 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"or": "or",
|
"or": "or",
|
||||||
|
"add": "Add",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"currency": "$"
|
"currency": "$",
|
||||||
|
"refresh": "Refresh"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
@@ -449,6 +451,7 @@
|
|||||||
"wheel": "Wheel",
|
"wheel": "Wheel",
|
||||||
"tariffs": "Tariffs",
|
"tariffs": "Tariffs",
|
||||||
"servers": "Servers",
|
"servers": "Servers",
|
||||||
|
"banSystem": "Ban Monitoring",
|
||||||
"broadcasts": "Broadcasts",
|
"broadcasts": "Broadcasts",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
"payments": "Payments",
|
"payments": "Payments",
|
||||||
@@ -464,6 +467,7 @@
|
|||||||
"wheelDesc": "Configure fortune wheel and prizes",
|
"wheelDesc": "Configure fortune wheel and prizes",
|
||||||
"tariffsDesc": "Manage tariff plans",
|
"tariffsDesc": "Manage tariff plans",
|
||||||
"serversDesc": "Configure VPN servers",
|
"serversDesc": "Configure VPN servers",
|
||||||
|
"banSystemDesc": "Ban monitoring and violations",
|
||||||
"broadcastsDesc": "Mass messaging to users",
|
"broadcastsDesc": "Mass messaging to users",
|
||||||
"usersDesc": "Manage bot users",
|
"usersDesc": "Manage bot users",
|
||||||
"paymentsDesc": "Payment verification",
|
"paymentsDesc": "Payment verification",
|
||||||
@@ -863,6 +867,283 @@
|
|||||||
"noTariffs": "No tariffs"
|
"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": {
|
"profile": {
|
||||||
"title": "Profile",
|
"title": "Profile",
|
||||||
"accountInfo": "Account Information",
|
"accountInfo": "Account Information",
|
||||||
|
|||||||
@@ -15,10 +15,12 @@
|
|||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"or": "или",
|
"or": "или",
|
||||||
|
"add": "Добавить",
|
||||||
"and": "и",
|
"and": "и",
|
||||||
"edit": "Редактировать",
|
"edit": "Редактировать",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"currency": "₽"
|
"currency": "₽",
|
||||||
|
"refresh": "Обновить"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Главная",
|
"dashboard": "Главная",
|
||||||
@@ -449,6 +451,7 @@
|
|||||||
"wheel": "Колесо",
|
"wheel": "Колесо",
|
||||||
"tariffs": "Тарифы",
|
"tariffs": "Тарифы",
|
||||||
"servers": "Серверы",
|
"servers": "Серверы",
|
||||||
|
"banSystem": "Мониторинг банов",
|
||||||
"broadcasts": "Рассылки",
|
"broadcasts": "Рассылки",
|
||||||
"users": "Пользователи",
|
"users": "Пользователи",
|
||||||
"payments": "Платежи",
|
"payments": "Платежи",
|
||||||
@@ -464,6 +467,7 @@
|
|||||||
"wheelDesc": "Настройка колеса удачи и призов",
|
"wheelDesc": "Настройка колеса удачи и призов",
|
||||||
"tariffsDesc": "Управление тарифными планами",
|
"tariffsDesc": "Управление тарифными планами",
|
||||||
"serversDesc": "Настройка VPN серверов",
|
"serversDesc": "Настройка VPN серверов",
|
||||||
|
"banSystemDesc": "Мониторинг банов и нарушений",
|
||||||
"broadcastsDesc": "Массовая отправка сообщений",
|
"broadcastsDesc": "Массовая отправка сообщений",
|
||||||
"usersDesc": "Управление пользователями бота",
|
"usersDesc": "Управление пользователями бота",
|
||||||
"paymentsDesc": "Проверка платежей",
|
"paymentsDesc": "Проверка платежей",
|
||||||
@@ -863,6 +867,283 @@
|
|||||||
"noTariffs": "Нет тарифов"
|
"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": {
|
"adminUsers": {
|
||||||
"title": "Пользователи",
|
"title": "Пользователи",
|
||||||
"subtitle": "Управление пользователями",
|
"subtitle": "Управление пользователями",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useState, useRef } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
adminAppsApi,
|
adminAppsApi,
|
||||||
AppDefinition,
|
AppDefinition,
|
||||||
@@ -14,6 +15,12 @@ import {
|
|||||||
} from '../api/adminApps'
|
} from '../api/adminApps'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
const AppsIcon = () => (
|
const AppsIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
|
||||||
@@ -640,7 +647,12 @@ export default function AdminApps() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AppsIcon />
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.apps.title')}</h1>
|
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.apps.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
1199
src/pages/AdminBanSystem.tsx
Normal file
1199
src/pages/AdminBanSystem.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useRef, useMemo } from 'react'
|
import { useState, useRef, useMemo } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
adminBroadcastsApi,
|
adminBroadcastsApi,
|
||||||
Broadcast,
|
Broadcast,
|
||||||
@@ -10,6 +11,12 @@ import {
|
|||||||
} from '../api/adminBroadcasts'
|
} from '../api/adminBroadcasts'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
const BroadcastIcon = () => (
|
const BroadcastIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-6 h-6" 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" />
|
<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" />
|
||||||
@@ -618,9 +625,12 @@ export default function AdminBroadcasts() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-accent-500/20 rounded-lg text-accent-400">
|
<Link
|
||||||
<BroadcastIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.title')}</h1>
|
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.broadcasts.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.broadcasts.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
campaignsApi,
|
campaignsApi,
|
||||||
CampaignListItem,
|
CampaignListItem,
|
||||||
@@ -14,9 +15,9 @@ import {
|
|||||||
} from '../api/campaigns'
|
} from '../api/campaigns'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const CampaignIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 3v11.25A2.25 2.25 0 006 16.5h2.25M3.75 3h-1.5m1.5 0h16.5m0 0h1.5m-1.5 0v11.25A2.25 2.25 0 0118 16.5h-2.25m-7.5 0h7.5m-7.5 0l-1 3m8.5-3l1 3m0 0l.5 1.5m-.5-1.5h-9.5m0 0l-.5 1.5M9 11.25v1.5M12 9v3.75m3-6v6" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -741,9 +742,12 @@ export default function AdminCampaigns() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-orange-500/20 rounded-lg text-orange-400">
|
<Link
|
||||||
<CampaignIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">Рекламные кампании</h1>
|
<h1 className="text-xl font-semibold text-dark-100">Рекламные кампании</h1>
|
||||||
<p className="text-sm text-dark-400">Управление рекламными ссылками</p>
|
<p className="text-sm text-dark-400">Управление рекламными ссылками</p>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { statsApi, type DashboardStats, type NodeStatus } from '../api/admin'
|
import { statsApi, type DashboardStats, type NodeStatus } from '../api/admin'
|
||||||
import { useCurrency } from '../hooks/useCurrency'
|
import { useCurrency } from '../hooks/useCurrency'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const ChartIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,9 +293,12 @@ export default function AdminDashboard() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-3 bg-accent-500/20 rounded-xl">
|
<Link
|
||||||
<ChartIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1>
|
<h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1>
|
||||||
<p className="text-dark-400">{t('adminDashboard.subtitle')}</p>
|
<p className="text-dark-400">{t('adminDashboard.subtitle')}</p>
|
||||||
|
|||||||
@@ -51,6 +51,12 @@ const ChartIcon = ({ className = "w-8 h-8" }: { className?: string }) => (
|
|||||||
</svg>
|
</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 }) => (
|
const BroadcastIcon = ({ className = "w-8 h-8" }: { className?: string }) => (
|
||||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<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" />
|
<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',
|
bgColor: 'bg-indigo-500/20',
|
||||||
textColor: 'text-indigo-400'
|
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',
|
to: '/admin/payments',
|
||||||
icon: <PaymentsIcon />,
|
icon: <PaymentsIcon />,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
promoOffersApi,
|
promoOffersApi,
|
||||||
PromoOfferTemplate,
|
PromoOfferTemplate,
|
||||||
@@ -12,9 +13,9 @@ import {
|
|||||||
} from '../api/promoOffers'
|
} from '../api/promoOffers'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const GiftIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 11.25v8.25a1.5 1.5 0 01-1.5 1.5H5.25a1.5 1.5 0 01-1.5-1.5v-8.25M12 4.875A2.625 2.625 0 109.375 7.5H12m0-2.625V7.5m0-2.625A2.625 2.625 0 1114.625 7.5H12m0 0V21m-8.625-9.75h18c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125h-18c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -576,9 +577,12 @@ export default function AdminPromoOffers() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-orange-500/20 rounded-lg text-orange-400">
|
<Link
|
||||||
<GiftIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">Промопредложения</h1>
|
<h1 className="text-xl font-semibold text-dark-100">Промопредложения</h1>
|
||||||
<p className="text-sm text-dark-400">Шаблоны, рассылка и логи предложений</p>
|
<p className="text-sm text-dark-400">Шаблоны, рассылка и логи предложений</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
promocodesApi,
|
promocodesApi,
|
||||||
PromoCode,
|
PromoCode,
|
||||||
@@ -13,9 +14,9 @@ import {
|
|||||||
} from '../api/promocodes'
|
} from '../api/promocodes'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const TicketIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 010 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 010-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -800,9 +801,12 @@ export default function AdminPromocodes() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-violet-500/20 rounded-lg">
|
<Link
|
||||||
<TicketIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">Промокоды</h1>
|
<h1 className="text-xl font-semibold text-dark-100">Промокоды</h1>
|
||||||
<p className="text-sm text-dark-400">Управление промокодами и группами скидок</p>
|
<p className="text-sm text-dark-400">Управление промокодами и группами скидок</p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
serversApi,
|
serversApi,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
@@ -9,9 +10,9 @@ import {
|
|||||||
} from '../api/servers'
|
} from '../api/servers'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const ServerIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 01-3-3m3 3a3 3 0 100 6h13.5a3 3 0 100-6m-16.5-3a3 3 0 013-3h13.5a3 3 0 013 3m-19.5 0a4.5 4.5 0 01.9-2.7L5.737 5.1a3.375 3.375 0 012.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 01.9 2.7m0 0a3 3 0 01-3 3m0 3h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008zm-3 6h.008v.008h-.008v-.008zm0-6h.008v.008h-.008v-.008z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -322,9 +323,12 @@ export default function AdminServers() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-accent-500/20 rounded-lg">
|
<Link
|
||||||
<ServerIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.servers.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.servers.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.servers.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.servers.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useMemo, useRef } from 'react'
|
import { useState, useMemo, useRef } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { adminSettingsApi, SettingDefinition, SettingCategorySummary } from '../api/adminSettings'
|
import { adminSettingsApi, SettingDefinition, SettingCategorySummary } from '../api/adminSettings'
|
||||||
import { brandingApi } from '../api/branding'
|
import { brandingApi } from '../api/branding'
|
||||||
import { themeColorsApi } from '../api/themeColors'
|
import { themeColorsApi } from '../api/themeColors'
|
||||||
@@ -10,6 +11,12 @@ import { applyThemeColors } from '../hooks/useThemeColors'
|
|||||||
import { updateEnabledThemesCache } from '../hooks/useTheme'
|
import { updateEnabledThemesCache } from '../hooks/useTheme'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
const CogIcon = () => (
|
const CogIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||||
@@ -1033,7 +1040,12 @@ export default function AdminSettings() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<CogIcon />
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-dark-50">{t('admin.settings.title')}</h1>
|
<h1 className="text-2xl font-bold text-dark-50">{t('admin.settings.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">
|
<p className="text-sm text-dark-400">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
tariffsApi,
|
tariffsApi,
|
||||||
TariffListItem,
|
TariffListItem,
|
||||||
@@ -13,9 +14,9 @@ import {
|
|||||||
} from '../api/tariffs'
|
} from '../api/tariffs'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const TariffIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 8.25h19.5M2.25 9h19.5m-16.5 5.25h6m-6 2.25h3m-3.75 3h15a2.25 2.25 0 002.25-2.25V6.75A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25v10.5A2.25 2.25 0 004.5 19.5z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1187,9 +1188,12 @@ export default function AdminTariffs() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2 bg-accent-500/20 rounded-lg">
|
<Link
|
||||||
<TariffIcon />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin'
|
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin'
|
||||||
import { ticketsApi } from '../api/tickets'
|
import { ticketsApi } from '../api/tickets'
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
function AdminMessageMedia({ message, t }: { message: AdminTicketMessage; t: (key: string) => string }) {
|
function AdminMessageMedia({ message, t }: { message: AdminTicketMessage; t: (key: string) => string }) {
|
||||||
const [imageLoaded, setImageLoaded] = useState(false)
|
const [imageLoaded, setImageLoaded] = useState(false)
|
||||||
const [imageError, setImageError] = useState(false)
|
const [imageError, setImageError] = useState(false)
|
||||||
@@ -175,7 +182,15 @@ export default function AdminTickets() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.tickets.title')}</h1>
|
<div className="flex items-center gap-3">
|
||||||
|
<Link
|
||||||
|
to="/admin"
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('admin.tickets.title')}</h1>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowSettings(true)}
|
onClick={() => setShowSettings(true)}
|
||||||
className="btn-secondary flex items-center gap-2"
|
className="btn-secondary flex items-center gap-2"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react'
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { useCurrency } from '../hooks/useCurrency'
|
import { useCurrency } from '../hooks/useCurrency'
|
||||||
import {
|
import {
|
||||||
adminUsersApi,
|
adminUsersApi,
|
||||||
@@ -11,12 +12,6 @@ import {
|
|||||||
|
|
||||||
// ============ Icons ============
|
// ============ Icons ============
|
||||||
|
|
||||||
const UsersIcon = ({ className = "w-6 h-6" }: { className?: string }) => (
|
|
||||||
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
|
|
||||||
</svg>
|
|
||||||
)
|
|
||||||
|
|
||||||
const SearchIcon = () => (
|
const SearchIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||||
@@ -861,9 +856,12 @@ export default function AdminUsers() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="p-2.5 bg-indigo-500/20 rounded-xl">
|
<Link
|
||||||
<UsersIcon className="w-7 h-7 text-indigo-400" />
|
to="/admin"
|
||||||
</div>
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-dark-100">Пользователи</h1>
|
<h1 className="text-xl font-bold text-dark-100">Пользователи</h1>
|
||||||
<p className="text-sm text-dark-400">Управление пользователями бота</p>
|
<p className="text-sm text-dark-400">Управление пользователями бота</p>
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
import { adminWheelApi, type WheelPrizeAdmin } from '../api/wheel'
|
import { adminWheelApi, type WheelPrizeAdmin } from '../api/wheel'
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
const CogIcon = () => (
|
const CogIcon = () => (
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||||
@@ -113,9 +120,17 @@ export default function AdminWheel() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">
|
<div className="flex items-center gap-3">
|
||||||
{t('admin.wheel.title')}
|
<Link
|
||||||
</h1>
|
to="/admin"
|
||||||
|
className="w-10 h-10 flex items-center justify-center rounded-xl bg-dark-800 border border-dark-700 hover:border-dark-600 transition-colors"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">
|
||||||
|
{t('admin.wheel.title')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className={`px-3 py-1 rounded-full text-sm ${
|
<span className={`px-3 py-1 rounded-full text-sm ${
|
||||||
config.is_enabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
config.is_enabled ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react'
|
import { useState, useEffect, useMemo, useCallback } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useLocation } from 'react-router-dom'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
import { brandingApi, type BrandingInfo } from '../api/branding'
|
import { brandingApi, type BrandingInfo } from '../api/branding'
|
||||||
|
import { getAndClearReturnUrl } from '../utils/token'
|
||||||
import LanguageSwitcher from '../components/LanguageSwitcher'
|
import LanguageSwitcher from '../components/LanguageSwitcher'
|
||||||
import TelegramLoginButton from '../components/TelegramLoginButton'
|
import TelegramLoginButton from '../components/TelegramLoginButton'
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ const cacheBranding = (data: BrandingInfo) => {
|
|||||||
export default function Login() {
|
export default function Login() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore()
|
const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore()
|
||||||
const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram')
|
const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram')
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
@@ -54,6 +56,22 @@ export default function Login() {
|
|||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isTelegramWebApp, setIsTelegramWebApp] = useState(false)
|
const [isTelegramWebApp, setIsTelegramWebApp] = useState(false)
|
||||||
|
|
||||||
|
// Получаем URL для возврата после авторизации
|
||||||
|
const getReturnUrl = useCallback(() => {
|
||||||
|
// Сначала проверяем state от React Router
|
||||||
|
const stateFrom = (location.state as { from?: string })?.from
|
||||||
|
if (stateFrom && stateFrom !== '/login') {
|
||||||
|
return stateFrom
|
||||||
|
}
|
||||||
|
// Затем проверяем сохранённый URL в sessionStorage (от safeRedirectToLogin)
|
||||||
|
const savedUrl = getAndClearReturnUrl()
|
||||||
|
if (savedUrl && savedUrl !== '/login') {
|
||||||
|
return savedUrl
|
||||||
|
}
|
||||||
|
// По умолчанию на главную
|
||||||
|
return '/'
|
||||||
|
}, [location.state])
|
||||||
|
|
||||||
// Fetch branding
|
// Fetch branding
|
||||||
const cachedBranding = useMemo(() => getCachedBranding(), [])
|
const cachedBranding = useMemo(() => getCachedBranding(), [])
|
||||||
|
|
||||||
@@ -82,9 +100,9 @@ export default function Login() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
navigate('/')
|
navigate(getReturnUrl(), { replace: true })
|
||||||
}
|
}
|
||||||
}, [isAuthenticated, navigate])
|
}, [isAuthenticated, navigate, getReturnUrl])
|
||||||
|
|
||||||
// Try Telegram WebApp authentication on mount
|
// Try Telegram WebApp authentication on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -97,7 +115,7 @@ export default function Login() {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
try {
|
try {
|
||||||
await loginWithTelegram(tg.initData)
|
await loginWithTelegram(tg.initData)
|
||||||
navigate('/')
|
navigate(getReturnUrl(), { replace: true })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Telegram auth failed:', err)
|
console.error('Telegram auth failed:', err)
|
||||||
setError(t('auth.telegramRequired'))
|
setError(t('auth.telegramRequired'))
|
||||||
@@ -108,7 +126,7 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tryTelegramAuth()
|
tryTelegramAuth()
|
||||||
}, [loginWithTelegram, navigate, t])
|
}, [loginWithTelegram, navigate, t, getReturnUrl])
|
||||||
|
|
||||||
const handleEmailLogin = async (e: React.FormEvent) => {
|
const handleEmailLogin = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -117,7 +135,7 @@ export default function Login() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await loginWithEmail(email, password)
|
await loginWithEmail(email, password)
|
||||||
navigate('/')
|
navigate(getReturnUrl(), { replace: true })
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const error = err as { response?: { data?: { detail?: string } } }
|
const error = err as { response?: { data?: { detail?: string } } }
|
||||||
setError(error.response?.data?.detail || t('common.error'))
|
setError(error.response?.data?.detail || t('common.error'))
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
const newToken = await tokenRefreshManager.refreshAccessToken()
|
const newToken = await tokenRefreshManager.refreshAccessToken()
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
const user = await authApi.getMe()
|
const user = await authApi.getMe()
|
||||||
|
// Сначала проверяем admin статус, потом снимаем isLoading
|
||||||
|
await get().checkAdminStatus()
|
||||||
set({
|
set({
|
||||||
accessToken: newToken,
|
accessToken: newToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@@ -152,7 +154,6 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})
|
})
|
||||||
get().checkAdminStatus()
|
|
||||||
} else {
|
} else {
|
||||||
tokenStorage.clearTokens()
|
tokenStorage.clearTokens()
|
||||||
set({
|
set({
|
||||||
@@ -168,6 +169,8 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await authApi.getMe()
|
const user = await authApi.getMe()
|
||||||
|
// Сначала проверяем admin статус, потом снимаем isLoading
|
||||||
|
await get().checkAdminStatus()
|
||||||
set({
|
set({
|
||||||
accessToken,
|
accessToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@@ -175,13 +178,14 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})
|
})
|
||||||
get().checkAdminStatus()
|
|
||||||
} catch {
|
} catch {
|
||||||
// Token might be invalid on server, try to refresh
|
// Token might be invalid on server, try to refresh
|
||||||
const newToken = await tokenRefreshManager.refreshAccessToken()
|
const newToken = await tokenRefreshManager.refreshAccessToken()
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
try {
|
try {
|
||||||
const user = await authApi.getMe()
|
const user = await authApi.getMe()
|
||||||
|
// Сначала проверяем admin статус, потом снимаем isLoading
|
||||||
|
await get().checkAdminStatus()
|
||||||
set({
|
set({
|
||||||
accessToken: newToken,
|
accessToken: newToken,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
@@ -189,7 +193,6 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
isAuthenticated: true,
|
isAuthenticated: true,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
})
|
})
|
||||||
get().checkAdminStatus()
|
|
||||||
} catch {
|
} catch {
|
||||||
tokenStorage.clearTokens()
|
tokenStorage.clearTokens()
|
||||||
set({
|
set({
|
||||||
|
|||||||
@@ -252,9 +252,40 @@ class TokenRefreshManager {
|
|||||||
|
|
||||||
export const tokenRefreshManager = new TokenRefreshManager()
|
export const tokenRefreshManager = new TokenRefreshManager()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ключ для сохранения URL для возврата после логина
|
||||||
|
*/
|
||||||
|
const RETURN_URL_KEY = 'auth_return_url'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сохраняет URL для возврата после авторизации
|
||||||
|
*/
|
||||||
|
export function saveReturnUrl(): void {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const currentPath = window.location.pathname + window.location.search
|
||||||
|
// Не сохраняем /login как return URL
|
||||||
|
if (currentPath && currentPath !== '/login') {
|
||||||
|
sessionStorage.setItem(RETURN_URL_KEY, currentPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает и очищает сохранённый URL для возврата
|
||||||
|
*/
|
||||||
|
export function getAndClearReturnUrl(): string | null {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const url = sessionStorage.getItem(RETURN_URL_KEY)
|
||||||
|
sessionStorage.removeItem(RETURN_URL_KEY)
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Безопасный редирект на страницу логина
|
* Безопасный редирект на страницу логина
|
||||||
* Валидирует URL чтобы предотвратить open redirect уязвимость
|
* Валидирует URL чтобы предотвратить open redirect уязвимость
|
||||||
|
* Сохраняет текущий URL для возврата после авторизации
|
||||||
*/
|
*/
|
||||||
export function safeRedirectToLogin(): void {
|
export function safeRedirectToLogin(): void {
|
||||||
// Разрешённые пути для редиректа (относительные пути нашего приложения)
|
// Разрешённые пути для редиректа (относительные пути нашего приложения)
|
||||||
@@ -262,6 +293,8 @@ export function safeRedirectToLogin(): void {
|
|||||||
|
|
||||||
// Проверяем, что мы на том же origin
|
// Проверяем, что мы на том же origin
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
// Сохраняем текущий URL для возврата после логина
|
||||||
|
saveReturnUrl()
|
||||||
// Используем только относительный путь для безопасности
|
// Используем только относительный путь для безопасности
|
||||||
window.location.href = loginPath
|
window.location.href = loginPath
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user