Compare commits

...

36 Commits

Author SHA1 Message Date
fd84094aa4 Another attempt to fix .drone.yml...
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-22 10:37:05 +07:00
7eb2ea5662 An attempt to fix .drone.yml, deleted python-security stage
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-22 10:32:21 +07:00
de8ad67038 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-21 19:34:36 +06:00
7b1fe32871 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-21 19:30:53 +06:00
1a3fdf131c Fixed dockerfile
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-21 19:14:22 +06:00
fea553df3d fixed drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-18 20:52:38 +06:00
b4b5bba562 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 20:48:52 +06:00
49affe3891 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-18 20:27:54 +06:00
e66ecbf178 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 20:23:19 +06:00
d8f7f108c7 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 20:18:54 +06:00
217804b011 fixed docker-compose file
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 20:06:58 +06:00
643987d211 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 20:03:34 +06:00
5868c4014b fixed dockerfile
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 19:59:20 +06:00
e7b2216c72 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 19:56:38 +06:00
e4bbf50725 fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-18 19:48:21 +06:00
0ed8039644 Fixed dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-18 19:36:21 +06:00
4d9dfddd5d chore(docker): clean up docker-compose-simple.yml and remove backup file
All checks were successful
continuous-integration/drone/push Build is passing
- Remove inline comments from docker-compose-simple.yml for cleaner configuration
- Delete unused docker-compose.txt.backup file
- Remove unused volume definitions (servers-data, users-data)
- Add docker-compose-simple.yml to .gitignore to prevent accidental commits
- Simplify environment variable organization by removing comment separators
- Improve file maintainability by reducing comment clutter
2026-01-17 19:19:36 +06:00
9d19d49c9b chore(docker): clean up docker-compose formatting and whitespace
- Remove trailing whitespace from expose port configuration
- Consolidate environment variable declarations by removing extra blank lines
- Improve readability by standardizing spacing between configuration sections
- Maintain functional equivalence while improving code consistency
2026-01-17 19:18:20 +06:00
062984283a docs: Add comprehensive changelog for version 1.2.0
All checks were successful
continuous-integration/drone/push Build is passing
- Document all major changes including daemon system implementation
- Add migration guide for updating from previous versions
- Include deployment options and configuration details
- Document security improvements and performance optimizations
- Add troubleshooting and testing information

Major features added:
- Complete daemon system (like MCSManager)
- Nginx static file serving
- Enhanced authorization with role-based access
- Docker deployment improvements
- UI/UX enhancements
2026-01-17 11:22:07 +06:00
b781407334 feat: Add nginx configuration for static frontend serving
All checks were successful
continuous-integration/drone/push Build is passing
- Update docker-compose.yml to use nginx for static file serving
- Configure nginx to serve frontend static files and proxy API requests
- Add frontend-init container to copy static files to nginx volume
- Update nginx/default.conf with proper static file handling and gzip compression
- Add NGINX_SETUP.md documentation for nginx deployment
- Improve performance by separating static files from backend API

Changes:
- Frontend static files now served by nginx (better performance)
- Backend only handles API requests (port 8000, internal)
- Gzip compression and caching for static assets
- WebSocket support for console functionality
- Health check endpoint for monitoring
2026-01-17 11:18:21 +06:00
2d77f99e93 fix(docker): simplify deployment by removing nginx and exposing backend on port 80
All checks were successful
continuous-integration/drone/push Build is passing
- Remove nginx service from docker-compose.yml to eliminate configuration issues
- Expose backend directly on port 80 for direct access without reverse proxy
- Update BASE_URL and FRONTEND_URL environment variables to use port 80
- Add data volume mount for daemon storage at /app/data
- Add docker-compose.txt to .gitignore to exclude temporary files
- Add LINUX_DOCKER_FIX.md documentation with setup instructions and troubleshooting
- Simplify deployment configuration for Linux environments where nginx events section was causing startup failures
2026-01-17 10:56:53 +06:00
c0125f3962 fix(docker): simplify deployment with nginx fixes and alternative compose config
All checks were successful
continuous-integration/drone/push Build is passing
- Update nginx/default.conf with complete configuration including events section
- Add docker-compose-simple.yml for simplified deployment without nginx
- Update docker-compose.yml to properly mount nginx configuration
- Revise DOCKER_FIX.md with clearer instructions and recommended solutions
- Provide three deployment variants: with nginx, without nginx (recommended), and quick fix
- Include data folder structure and environment setup documentation
This change addresses nginx configuration errors and FileNotFoundError by providing both a corrected nginx setup and a simplified alternative deployment method without nginx for faster troubleshooting and deployment.
2026-01-17 10:50:00 +06:00
e02789ef53 fix(docker): resolve nginx and backend path configuration issues
All checks were successful
continuous-integration/drone/push Build is passing
- Add nginx/default.conf with simplified configuration to fix "no events section" error
- Update docker-compose.yml to mount nginx/default.conf instead of nginx.conf
- Fix backend/daemons.py data path from backend/data/daemons.json to data/daemons.json
- Improve users.json path detection with fallback logic in daemons.py
- Add servers directory to .gitignore
- Create DOCKER_FIX.md documentation with troubleshooting steps and solutions
- Ensure data directory is created automatically when backend starts
2026-01-17 10:32:46 +06:00
d188cec1f0 Added Daemon system and fixed interface
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-16 18:56:21 +06:00
fbfddf3c7a Changed design and bug fixes
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-16 15:40:14 +06:00
e6264efac6 Fixed drone.yml
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-15 22:21:14 +06:00
c840024e4a Fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-15 22:16:53 +06:00
fbb1356b13 Fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-15 22:08:28 +06:00
3a621b6d92 Fixed drone.yml
Some checks failed
continuous-integration/drone/push Build encountered an error
2026-01-15 21:41:14 +06:00
ca7882b84a Fixed Dockerfile
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-15 21:22:13 +06:00
66ece236f9 Merge branch 'main' of https://git.nevetime.ru/Arkon/NeveTime-Panel--MC-Panel-
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-15 20:58:16 +06:00
6d80ef7200 Fixed drone.yml 2026-01-15 20:57:51 +06:00
07df32dda8 Изменён адрес реестра изображений
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-15 20:56:14 +07:00
7aa13ba01c Deleted unnecessary text 2026-01-15 20:43:43 +07:00
1985a25ea8 Removed unused .md files 2026-01-15 19:38:21 +06:00
112123b0ff Change drone.yml 2026-01-15 19:25:42 +06:00
59 changed files with 3763 additions and 5357 deletions

View File

@@ -4,7 +4,7 @@
.gitattributes .gitattributes
# CI/CD # CI/CD
.drone.yml .drone.yml*
.github .github
# Documentation # Documentation
@@ -12,6 +12,7 @@
!README.md !README.md
CHANGELOG.md CHANGELOG.md
LICENSE LICENSE
ОБНОВЛЕНИЯ.md
# IDE # IDE
.vscode .vscode
@@ -24,9 +25,8 @@ LICENSE
.DS_Store .DS_Store
Thumbs.db Thumbs.db
# Node # Node.js
frontend/node_modules frontend/node_modules
frontend/dist
frontend/.vite frontend/.vite
frontend/npm-debug.log* frontend/npm-debug.log*
frontend/yarn-debug.log* frontend/yarn-debug.log*
@@ -46,34 +46,68 @@ backend/pip-delete-this-directory.txt
backend/.pytest_cache backend/.pytest_cache
backend/.coverage backend/.coverage
backend/htmlcov backend/htmlcov
daemon/__pycache__
# Data (будет монтироваться как volume) # Data (будет монтироваться как volume)
backend/servers/* backend/servers/*
backend/users.json !backend/servers/.gitkeep
backend/tickets.json backend/data/*
!backend/data/.gitkeep
# Logs # Logs
*.log *.log
logs logs/*
!logs/.gitkeep
# Environment # Environment files
.env .env
.env.local .env.local
.env.*.local .env.*.local
backend/.env*
frontend/.env*
daemon/.env*
# Docker # Docker
docker-compose.yml docker-compose.yml
docker-compose.*.yml docker-compose.*.yml
Dockerfile
.dockerignore .dockerignore
docker-start.bat
docker-stop.bat
# Tests # Tests
tests tests
test test
*.test.js *.test.js
*.spec.js *.spec.js
__tests__
# Build artifacts # Build artifacts
build build
dist dist
*.egg-info *.egg-info
.pytest_cache
# Temporary files
tmp
temp
*.tmp
*.temp
# Batch files (Windows specific)
*.bat
# Postman collections
*.postman_collection.json
# Nginx configs (handled separately)
nginx/*
# Keys and certificates
*.key
*.pem
*.crt
*.p12
# Backup files
*.bak
*.backup

View File

@@ -1,264 +1,160 @@
---
kind: pipeline kind: pipeline
type: docker type: docker
name: code-quality name: code-quality
# Триггеры для пайплайна проверки качества
trigger: trigger:
event: event:
- push - push
- pull_request - pull_request
steps: steps:
# Проверка качества Python кода
- name: python-lint - name: python-lint
image: python:3.11-slim image: python:3.11-slim
commands: commands:
- cd backend - cd backend
- pip install flake8 pylint black isort - pip install flake8
- echo "Running flake8..."
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- echo "Running pylint..."
- pylint **/*.py --exit-zero --max-line-length=127
- echo "Checking code formatting with black..."
- black --check --diff .
- echo "Checking imports with isort..."
- isort --check-only --diff .
# Проверка качества JavaScript/React кода
- name: frontend-lint - name: frontend-lint
image: node:18-alpine image: node:20-alpine
commands: commands:
- cd frontend - cd frontend
- npm ci - npm ci --silent
- echo "Running ESLint..." - npm run lint || echo "ESLint warnings found"
- npm run lint || true
- echo "Checking code formatting..."
- npx prettier --check "src/**/*.{js,jsx,ts,tsx,json,css,md}" || true
# Проверка безопасности зависимостей Python
- name: python-security
image: python:3.11-slim
commands:
- cd backend
- pip install safety bandit
- echo "Checking for known security vulnerabilities..."
- safety check --file=requirements.txt --exit-zero
- echo "Running bandit security linter..."
- bandit -r . -f json -o bandit-report.json --exit-zero || true
- bandit -r . --exit-zero
# Проверка безопасности зависимостей Node.js
- name: frontend-security - name: frontend-security
image: node:18-alpine image: node:20-alpine
commands: commands:
- cd frontend - cd frontend
- npm ci - npm ci --silent
- echo "Running npm audit..." - npm audit --audit-level=moderate || echo "Security warnings found"
- npm audit --audit-level=moderate || true
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: build-and-publish name: build-backend
# Триггеры для пайплайна сборки
trigger: trigger:
event:
- push
- tag
branch: branch:
- main - main
- master - master
- develop - develop
event:
- push
- tag
# Зависимость от пайплайна проверки качества
depends_on: depends_on:
- code-quality - code-quality
steps: steps:
# Сборка и публикация Docker образа - name: build-backend-image
- name: build-and-push
image: plugins/docker image: plugins/docker
settings: settings:
# Настройки реестра (замените на свои) registry: registry.nevetime.ru
registry: registry.example.com repo: registry.nevetime.ru/mc-panel-backend
repo: registry.example.com/mc-panel
# Теги для образа
tags: tags:
- latest - latest
- ${DRONE_COMMIT_SHA:0:8} - "${DRONE_COMMIT_SHA:0:8}"
- ${DRONE_BRANCH} - "${DRONE_BRANCH}"
# Автоматическое тегирование при push тега
auto_tag: true auto_tag: true
auto_tag_suffix: ${DRONE_BUILD_NUMBER} dockerfile: backend/Dockerfile
context: backend
# Dockerfile
dockerfile: Dockerfile
context: .
# Учетные данные (настройте в Drone secrets)
username: username:
from_secret: docker_username from_secret: docker_username
password: password:
from_secret: docker_password from_secret: docker_password
# Build args (опционально)
build_args: build_args:
- BUILD_DATE=${DRONE_BUILD_CREATED} - BUILD_DATE=${DRONE_BUILD_CREATED}
- VCS_REF=${DRONE_COMMIT_SHA} - VCS_REF=${DRONE_COMMIT_SHA}
- VERSION=${DRONE_TAG:-${DRONE_BRANCH}} - VERSION=${DRONE_TAG:-${DRONE_BRANCH}}
when: when:
event: event:
- push - push
- tag - tag
# Сканирование образа на уязвимости (опционально)
- name: scan-image
image: aquasec/trivy
commands:
- trivy image --exit-code 0 --severity HIGH,CRITICAL registry.example.com/mc-panel:${DRONE_COMMIT_SHA:0:8}
when:
event:
- push
- tag
depends_on:
- build-and-push
# Уведомление об успешной сборке (опционально)
- name: notify-success
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: deployments
username: drone
template: >
✅ Build #{{build.number}} succeeded!
Repository: {{repo.name}}
Branch: {{build.branch}}
Commit: {{build.commit}}
Author: {{build.author}}
Docker image: registry.example.com/mc-panel:{{build.commit}}
when:
status:
- success
event:
- push
- tag
depends_on:
- build-and-push
# Уведомление об ошибке (опционально)
- name: notify-failure
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: deployments
username: drone
template: >
❌ Build #{{build.number}} failed!
Repository: {{repo.name}}
Branch: {{build.branch}}
Commit: {{build.commit}}
Author: {{build.author}}
Link: {{build.link}}
when:
status:
- failure
event:
- push
- tag
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: deploy-staging name: build-frontend
# Пайплайн для деплоя на staging (опционально)
trigger: trigger:
event:
- push
branch: branch:
- main
- master
- develop - develop
event:
- push
- tag
depends_on: depends_on:
- build-and-publish - code-quality
steps: steps:
- name: deploy-to-staging - name: build-frontend-image
image: appleboy/drone-ssh image: plugins/docker
settings: settings:
host: registry: registry.nevetime.ru
from_secret: staging_host repo: registry.nevetime.ru/mc-panel-frontend
tags:
- latest
- "${DRONE_COMMIT_SHA:0:8}"
- "${DRONE_BRANCH}"
auto_tag: true
dockerfile: frontend/Dockerfile
context: frontend
target: production
username: username:
from_secret: staging_username from_secret: docker_username
key: password:
from_secret: staging_ssh_key from_secret: docker_password
port: 22 build_args:
script: - BUILD_DATE=${DRONE_BUILD_CREATED}
- cd /opt/mc-panel - VCS_REF=${DRONE_COMMIT_SHA}
- docker-compose pull - VERSION=${DRONE_TAG:-${DRONE_BRANCH}}
- docker-compose up -d when:
- docker-compose ps event:
- push
- tag
--- ---
kind: pipeline kind: pipeline
type: docker type: docker
name: deploy-production name: build-monolith
# Пайплайн для деплоя на production (только для тегов)
trigger: trigger:
branch:
- main
- master
- develop
event: event:
- push
- tag - tag
ref:
- refs/tags/v*
depends_on: depends_on:
- build-and-publish - code-quality
steps: steps:
- name: deploy-to-production - name: build-monolith-image
image: appleboy/drone-ssh image: plugins/docker
settings: settings:
host: registry: registry.nevetime.ru
from_secret: production_host repo: registry.nevetime.ru/mc-panel
tags:
- latest
- "${DRONE_COMMIT_SHA:0:8}"
- "${DRONE_BRANCH}"
auto_tag: true
dockerfile: Dockerfile
context: .
username: username:
from_secret: production_username from_secret: docker_username
key: password:
from_secret: production_ssh_key from_secret: docker_password
port: 22 build_args:
script: - BUILD_DATE=${DRONE_BUILD_CREATED}
- cd /opt/mc-panel - VCS_REF=${DRONE_COMMIT_SHA}
- docker-compose pull - VERSION=${DRONE_TAG:-${DRONE_BRANCH}}
- docker-compose up -d
- docker-compose ps
- echo "Deployed version ${DRONE_TAG}"
- name: notify-production-deploy
image: plugins/slack
settings:
webhook:
from_secret: slack_webhook
channel: deployments
username: drone
template: >
🚀 Production deployment successful!
Version: {{build.tag}}
Repository: {{repo.name}}
Author: {{build.author}}
Docker image: registry.example.com/mc-panel:{{build.tag}}
when: when:
status: event:
- success - push
- tag

128
.drone.yml.with-trivy Normal file
View File

@@ -0,0 +1,128 @@
---
kind: pipeline
type: docker
name: code-quality
# Триггеры для пайплайна проверки качества
trigger:
event:
- push
- pull_request
steps:
# Проверка качества Python кода (только критические ошибки)
- name: python-lint
image: python:3.11-slim
commands:
- cd backend
- pip install flake8
- echo "Running flake8 (critical errors only)..."
- flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- echo "✅ Critical checks passed"
# Проверка качества JavaScript/React кода (опционально)
- name: frontend-lint
image: node:18-alpine
commands:
- cd frontend
- npm ci
- echo "Running ESLint (non-blocking)..."
- npm run lint || echo "⚠️ ESLint warnings found (non-blocking)"
- echo "✅ Frontend checks completed"
# Проверка безопасности зависимостей Python (опционально)
- name: python-security
image: python:3.11-slim
commands:
- cd backend
- pip install safety
- echo "Checking for known security vulnerabilities..."
- safety check --file=requirements.txt --exit-zero || echo "⚠️ Security warnings found (non-blocking)"
- echo "✅ Security checks completed"
# Проверка безопасности зависимостей Node.js
- name: frontend-security
image: node:18-alpine
commands:
- cd frontend
- npm ci
- echo "Running npm audit..."
- npm audit --audit-level=moderate || true
---
kind: pipeline
type: docker
name: build-and-publish
# Триггеры для пайплайна сборки
trigger:
event:
- push
- tag
branch:
- main
- master
- develop
# Зависимость от пайплайна проверки качества
depends_on:
- code-quality
steps:
# Сборка и публикация Docker образа
- name: build-and-push
image: plugins/docker
settings:
# Настройки реестра
registry: registry.nevetime.ru
repo: registry.nevetime.ru/mc-panel
# Теги для образа
tags:
- latest
- ${DRONE_COMMIT_SHA:0:8}
- ${DRONE_BRANCH}
# Автоматическое тегирование при push тега
auto_tag: true
auto_tag_suffix: ${DRONE_BUILD_NUMBER}
# Dockerfile
dockerfile: Dockerfile
context: .
# Учетные данные (настройте в Drone secrets)
username:
from_secret: docker_username
password:
from_secret: docker_password
# Build args (опционально)
build_args:
- BUILD_DATE=${DRONE_BUILD_CREATED}
- VCS_REF=${DRONE_COMMIT_SHA}
- VERSION=${DRONE_TAG:-${DRONE_BRANCH}}
when:
event:
- push
- tag
# Сканирование образа на уязвимости (с авторизацией)
- name: scan-image
image: aquasec/trivy
environment:
TRIVY_USERNAME:
from_secret: docker_username
TRIVY_PASSWORD:
from_secret: docker_password
commands:
- echo "Scanning image for vulnerabilities..."
- trivy image --exit-code 0 --severity HIGH,CRITICAL --username $TRIVY_USERNAME --password $TRIVY_PASSWORD registry.nevetime.ru/mc-panel:${DRONE_COMMIT_SHA:0:8}
- echo "✅ Security scan completed"
when:
event:
- push
- tag
depends_on:
- build-and-push

View File

@@ -1,40 +0,0 @@
# MC Panel Environment Variables
# ZITADEL OpenID Connect
ZITADEL_ISSUER=https://your-instance.zitadel.cloud
ZITADEL_CLIENT_ID=your_client_id_here
ZITADEL_CLIENT_SECRET=your_client_secret_here
# Application URLs
BASE_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
# Security
# ВАЖНО: Измените это значение в production!
SECRET_KEY=your-very-long-random-secret-key-change-this-in-production
# Database (если используете)
# DATABASE_URL=postgresql://user:password@localhost:5432/mcpanel
# Redis (если используете для кеширования)
# REDIS_URL=redis://localhost:6379/0
# Email (для уведомлений, опционально)
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# SMTP_USER=your-email@gmail.com
# SMTP_PASSWORD=your-app-password
# SMTP_FROM=noreply@mcpanel.com
# Logging
# LOG_LEVEL=INFO
# LOG_FILE=/var/log/mcpanel/app.log
# Features
# ENABLE_REGISTRATION=true
# ENABLE_OIDC=true
# MAX_SERVERS_PER_USER=10
# Performance
# WORKERS=4
# MAX_UPLOAD_SIZE=100MB

4
.gitignore vendored
View File

@@ -17,6 +17,7 @@ dist/
# Servers # Servers
backend/servers/ backend/servers/
backend/.env.exemple backend/.env.exemple
servers
# IDE # IDE
.vscode/ .vscode/
@@ -35,3 +36,6 @@ frontend/.env.production.local
# Build # Build
frontend/dist/ frontend/dist/
backend/build/ backend/build/
backend/users1.json.backup
docker-compose.txt
docker-compose-simple.yml

531
API.md
View File

@@ -1,531 +0,0 @@
# MC Panel API - Полная документация
**Версия:** 1.0.0
**Дата:** 15 января 2026
---
## 📋 Содержание
1. [Базовая информация](#базовая-информация)
2. [Быстрый старт](#быстрый-старт)
3. [Аутентификация](#аутентификация)
4. [Управление пользователями](#управление-пользователями)
5. [Личный кабинет](#личный-кабинет)
6. [Управление серверами](#управление-серверами)
7. [Управление файлами](#управление-файлами)
8. [Тикеты](#тикеты)
9. [OpenID Connect](#openid-connect)
10. [Коды ошибок](#коды-ошибок)
11. [Примеры интеграции](#примеры-интеграции)
12. [Postman коллекция](#postman-коллекция)
---
## Базовая информация
**Base URL:** `http://localhost:8000`
**Формат данных:** JSON
**Аутентификация:** Bearer Token (JWT)
Все защищенные эндпоинты требуют заголовок:
```
Authorization: Bearer <token>
```
### Заголовки запросов
```
Content-Type: application/json
Authorization: Bearer <token>
```
### Формат ответов
```json
{
"message": "Success message",
"data": {}
}
```
---
## Быстрый старт
### 1. Регистрация
```bash
curl -X POST http://localhost:8000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password123"}'
```
### 2. Вход
```bash
curl -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"password123"}'
```
**Ответ:**
```json
{
"access_token": "eyJhbGc...",
"token_type": "bearer",
"username": "admin",
"role": "admin"
}
```
### 3. Использование токена
```bash
TOKEN="your_token_here"
curl http://localhost:8000/api/servers \
-H "Authorization: Bearer $TOKEN"
```
---
## Аутентификация
### POST /api/auth/register
Регистрация нового пользователя.
**Body:**
```json
{
"username": "string",
"password": "string"
}
```
**Response (200):**
```json
{
"access_token": "string",
"token_type": "bearer",
"username": "string",
"role": "admin|user"
}
```
---
### POST /api/auth/login
Вход в систему.
**Body:**
```json
{
"username": "string",
"password": "string"
}
```
**Response (200):**
```json
{
"access_token": "string",
"token_type": "bearer",
"username": "string",
"role": "admin|user|support|banned"
}
```
**Errors:**
- `401` - Неверные учетные данные
---
### GET /api/auth/me
Получить информацию о текущем пользователе.
**Headers:** `Authorization: Bearer <token>`
**Response (200):**
```json
{
"username": "string",
"role": "admin|user|support|banned",
"servers": ["server1", "server2"]
}
```
---
## Управление пользователями
### GET /api/users
Список всех пользователей.
### PUT /api/users/{username}/role
Изменить роль пользователя (admin only).
**Body:** `{"role": "admin|user|support|banned"}`
### PUT /api/users/{username}/servers
Управление доступом к серверам.
**Body:** `{"servers": ["server1", "server2"]}`
### DELETE /api/users/{username}
Удалить пользователя (admin only).
---
## Личный кабинет
### GET /api/profile/stats
Статистика текущего пользователя.
### GET /api/profile/stats/{username}
Статистика другого пользователя (admin/support).
### PUT /api/profile/username
Изменить имя пользователя.
**Body:** `{"new_username": "string", "password": "string"}`
### PUT /api/profile/password
Изменить пароль.
**Body:** `{"old_password": "string", "new_password": "string"}`
---
## Управление серверами
### GET /api/servers
Список серверов пользователя.
### POST /api/servers/create
Создать новый сервер.
**Body:**
```json
{
"name": "server1",
"displayName": "My Server",
"startCommand": "java -Xmx2G -jar server.jar nogui"
}
```
### GET /api/servers/{server}/config
Получить конфигурацию сервера.
### PUT /api/servers/{server}/config
Обновить конфигурацию сервера.
### DELETE /api/servers/{server}
Удалить сервер (admin only).
### POST /api/servers/{server}/start
Запустить сервер.
### POST /api/servers/{server}/stop
Остановить сервер.
### POST /api/servers/{server}/command
Отправить команду серверу.
**Body:** `{"command": "say Hello"}`
### GET /api/servers/{server}/stats
Получить статистику сервера (CPU, RAM, Disk).
### WS /ws/servers/{server}/console
WebSocket для консоли сервера (логи в реальном времени).
---
## Управление файлами
### GET /api/servers/{server}/files?path={path}
Список файлов в директории.
### POST /api/servers/{server}/files/create
Создать файл или папку.
**Body:** `{"type": "file|folder", "name": "string", "path": "string"}`
### POST /api/servers/{server}/files/upload?path={path}
Загрузить файл (multipart/form-data).
### GET /api/servers/{server}/files/download?path={path}
Скачать файл.
### GET /api/servers/{server}/files/content?path={path}
Получить содержимое текстового файла.
### PUT /api/servers/{server}/files/content?path={path}
Сохранить содержимое файла.
**Body:** `{"content": "string"}`
### PUT /api/servers/{server}/files/rename?old_path={path}&new_name={name}
Переименовать файл.
### POST /api/servers/{server}/files/move
Переместить файл.
**Body:** `{"source": "path", "destination": "path"}`
### DELETE /api/servers/{server}/files?path={path}
Удалить файл или папку.
---
## Тикеты
### GET /api/tickets
Список тикетов (свои или все для admin/support).
### POST /api/tickets/create
Создать новый тикет.
**Body:** `{"title": "string", "description": "string"}`
### GET /api/tickets/{id}
Получить тикет по ID.
### POST /api/tickets/{id}/message
Добавить сообщение в тикет.
**Body:** `{"text": "string"}`
### PUT /api/tickets/{id}/status
Изменить статус тикета (admin/support).
**Body:** `{"status": "pending|in_progress|closed"}`
---
## OpenID Connect
### GET /api/auth/oidc/providers
Список доступных OIDC провайдеров.
### GET /api/auth/oidc/{provider}/login
Начать аутентификацию через OIDC (redirect).
### GET /api/auth/oidc/{provider}/callback
Callback от OIDC провайдера (redirect).
---
## Коды ошибок
| Код | Описание | Решение |
|-----|----------|---------|
| 200 | Успешно | - |
| 400 | Неверный запрос | Проверьте формат данных |
| 401 | Не авторизован | Войдите в систему |
| 403 | Доступ запрещен | Недостаточно прав |
| 404 | Не найдено | Проверьте URL |
| 500 | Ошибка сервера | Обратитесь к администратору |
---
## Примеры интеграции
### Python
```python
import requests
class MCPanelAPI:
def __init__(self, base_url, username, password):
self.base_url = base_url
self.token = None
self.login(username, password)
def login(self, username, password):
r = requests.post(f"{self.base_url}/api/auth/login",
json={"username": username, "password": password})
self.token = r.json()["access_token"]
def get_headers(self):
return {"Authorization": f"Bearer {self.token}"}
def get_servers(self):
r = requests.get(f"{self.base_url}/api/servers",
headers=self.get_headers())
return r.json()
def start_server(self, server_name):
r = requests.post(
f"{self.base_url}/api/servers/{server_name}/start",
headers=self.get_headers())
return r.json()
# Использование
api = MCPanelAPI("http://localhost:8000", "admin", "password")
servers = api.get_servers()
api.start_server("survival")
```
---
### JavaScript
```javascript
class MCPanelAPI {
constructor(baseURL) {
this.baseURL = baseURL;
this.token = null;
}
async login(username, password) {
const response = await fetch(`${this.baseURL}/api/auth/login`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password})
});
const data = await response.json();
this.token = data.access_token;
}
getHeaders() {
return {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
};
}
async getServers() {
const response = await fetch(`${this.baseURL}/api/servers`, {
headers: this.getHeaders()
});
return await response.json();
}
async startServer(serverName) {
const response = await fetch(
`${this.baseURL}/api/servers/${serverName}/start`,
{method: 'POST', headers: this.getHeaders()}
);
return await response.json();
}
}
// Использование
const api = new MCPanelAPI('http://localhost:8000');
await api.login('admin', 'password');
const servers = await api.getServers();
await api.startServer('survival');
```
---
### cURL примеры
```bash
# Вход
TOKEN=$(curl -s -X POST http://localhost:8000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"pass"}' \
| jq -r '.access_token')
# Список серверов
curl http://localhost:8000/api/servers \
-H "Authorization: Bearer $TOKEN"
# Создать сервер
curl -X POST http://localhost:8000/api/servers/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"survival","displayName":"Survival","startCommand":"java -jar server.jar"}'
# Запустить сервер
curl -X POST http://localhost:8000/api/servers/survival/start \
-H "Authorization: Bearer $TOKEN"
# Отправить команду
curl -X POST http://localhost:8000/api/servers/survival/command \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"command":"say Hello"}'
# Список файлов
curl "http://localhost:8000/api/servers/survival/files?path=plugins" \
-H "Authorization: Bearer $TOKEN"
# Создать тикет
curl -X POST http://localhost:8000/api/tickets/create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title":"Problem","description":"Details"}'
```
---
## Postman коллекция
### Импорт коллекции
1. Откройте Postman
2. File → Import
3. Выберите файл `MC_Panel_API.postman_collection.json`
4. Коллекция готова к использованию
### Настройка переменных
В коллекции настройте переменные:
- `baseUrl` = `http://localhost:8000`
- `serverName` = `survival` (или имя вашего сервера)
- `token` = автоматически сохраняется после Login
### Использование
1. Выполните запрос "Login" для получения токена
2. Токен автоматически сохранится в переменную `token`
3. Все остальные запросы будут использовать этот токен
4. Используйте любые эндпоинты из коллекции
### Структура коллекции
- **Authentication** - регистрация, вход, получение пользователя
- **Users** - управление пользователями
- **Servers** - управление серверами
- **Files** - операции с файлами
- **Tickets** - система тикетов
- **Profile** - личный кабинет
- **OpenID Connect** - OIDC провайдеры
---
## Безопасность
### JWT Токены
- Срок действия: 7 дней
- Алгоритм: HS256
- Хранение: localStorage (фронтенд)
### Рекомендации
1. Используйте HTTPS в production
2. Измените SECRET_KEY в `backend/main.py`
3. Используйте сильные пароли (минимум 6 символов)
4. Регулярно обновляйте зависимости
5. Ограничьте CORS для конкретных доменов
---
## Лимиты и ограничения
- **Размер файла:** не ограничен (зависит от сервера)
- **Количество запросов:** не ограничено
- **Длина сообщения:** не ограничена
- **Количество серверов:** не ограничено
- **Срок хранения логов:** 1000 последних строк
---
## Changelog
### 1.0.0 (15.01.2026)
- ✨ Первый релиз API
- ✅ 37 эндпоинтов
- ✅ JWT аутентификация
- ✅ OpenID Connect
- ✅ WebSocket консоль
- ✅ Полное управление серверами
- ✅ Файловый менеджер
- ✅ Система тикетов
---
## Поддержка
- **Документация проекта:** ДОКУМЕНТАЦИЯ.md
- **Postman коллекция:** MC_Panel_API.postman_collection.json
- **Тикеты:** Используйте систему тикетов в панели
---
**Версия API:** 1.0.0
**Дата обновления:** 15 января 2026
**Спасибо за использование MC Panel API!** 🚀

View File

@@ -1,176 +0,0 @@
# Changelog - История изменений MC Panel
Все значимые изменения в проекте документируются в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
и проект следует [Semantic Versioning](https://semver.org/lang/ru/).
---
## [1.1.0] - 2026-01-15
### Добавлено ✨
#### Система прав и ролей
- **Роль владельца (Owner)** - полный контроль над панелью
- **Система прав** - детальное управление возможностями пользователей
- **5 ролей**: Owner, Admin, Support, User, Banned
- **7 типов прав**: manage_users, manage_roles, manage_servers, manage_tickets, manage_files, delete_users, view_all_resources
#### API эндпоинты
- `GET /api/users` - Получить список пользователей
- `PUT /api/users/{user_id}/role` - Изменить роль пользователя
- `PUT /api/users/{user_id}/permissions` - Изменить права пользователя
- `POST /api/users/{user_id}/access/servers` - Выдать доступ к серверу
- `DELETE /api/users/{user_id}/access/servers/{server_name}` - Забрать доступ к серверу
- `DELETE /api/users/{user_id}` - Удалить пользователя
- `POST /api/users/{user_id}/ban` - Заблокировать пользователя
- `POST /api/users/{user_id}/unban` - Разблокировать пользователя
#### Инструменты
- **migrate_users.py** - Скрипт миграции пользователей на новую систему
- **MIGRATE_USERS.bat** - Bat файл для запуска миграции на Windows
- **OWNER_PERMISSIONS.md** - Полная документация системы прав (~500 строк)
#### Документация
- Документация системы прав и ролей
- Примеры использования API на Python, JavaScript, cURL
- FAQ по системе прав
- Инструкции по миграции
### Изменено 🔄
- Первый зарегистрированный пользователь теперь получает роль `owner` вместо `admin`
- Обновлена структура пользователя в `users.json`:
- Добавлено поле `permissions` с детальными правами
- Добавлено поле `resource_access` для управления доступом к ресурсам
- Все эндпоинты управления пользователями теперь проверяют права доступа
- Обновлена версия проекта с 1.0.0 до 1.1.0
### Безопасность 🔒
- Добавлена проверка прав для всех административных эндпоинтов
- Логирование всех действий владельца
- Защита от удаления владельца
- Автоматическое понижение роли при передаче прав владельца
---
## [1.0.0] - 2026-01-15
### Добавлено ✨
#### Backend (FastAPI)
- REST API с 37 эндпоинтами
- JWT аутентификация (токены на 7 дней)
- OpenID Connect интеграция (ZITADEL)
- WebSocket для real-time консоли
- Управление Minecraft серверами
- Файловый менеджер API
- Система тикетов
- Роли пользователей (admin, user)
- Bcrypt хеширование паролей
#### Frontend (React)
- 6 тем оформления (modern, dark, light, blue, green, purple)
- Цветная консоль (INFO=зелёный, WARN=жёлтый, ERROR=красный)
- Файловый менеджер с поиском
- Создание файлов и папок
- Перемещение файлов (Cut/Paste)
- Система уведомлений (4 типа: success, error, warning, info)
- Тикеты с real-time обновлениями (каждые 3 секунды)
- Статистика серверов (CPU, RAM, диск)
- Личный кабинет
- Responsive дизайн
#### Docker & DevOps
- Multi-stage Dockerfile (Node.js + Python)
- Docker Compose конфигурация
- Non-root пользователь для безопасности
- Healthcheck для мониторинга
- Volumes для персистентности данных
- Nginx reverse proxy с SSL/TLS
- Security headers (HSTS, X-Frame-Options, etc.)
- Rate limiting для защиты от DDoS
- 4 CI/CD пайплайна (Drone):
- code-quality - Проверка качества кода
- build-and-publish - Сборка и публикация образа
- deploy-staging - Деплой на staging
- deploy-production - Деплой на production
#### Bat файлы (Windows)
- START_PANEL.bat - Локальный запуск
- START_DOCKER.bat - Запуск в Docker
- STOP_DOCKER.bat - Остановка Docker
- RESTART_DOCKER.bat - Перезапуск Docker
- LOGS_DOCKER.bat - Просмотр логов
- UPDATE_DOCKER.bat - Обновление Docker образа
- BACKUP_DATA.bat - Создание backup
- RESTORE_DATA.bat - Восстановление из backup
#### Документация
- README.md - Главная навигация (~200 строк)
- ДОКУМЕНТАЦИЯ.md - Полное руководство (~500 строк)
- API.md - API документация (~300 строк)
- DOCKER.md - Docker и CI/CD (~400 строк)
- DOCKER_COMMANDS.md - Docker команды (~300 строк)
- DOCKER_ГОТОВО.md - Docker summary (~400 строк)
- ГОТОВО.md - История разработки (~200 строк)
- ПРОЕКТ_ЗАВЕРШЁН.md - Полный обзор (~600 строк)
- ФИНАЛЬНЫЙ_СПИСОК.md - Список файлов (~700 строк)
- INSTALL_DOCKER.md - Установка Docker (~400 строк)
- FAQ.md - Часто задаваемые вопросы (~500 строк)
- BAT_FILES.md - Описание bat файлов (~400 строк)
- QUICKSTART.md - Быстрый старт (~300 строк)
- РАБОТА_ЗАВЕРШЕНА.md - Финальный summary (~200 строк)
- CHECKLIST.md - Финальный checklist (~400 строк)
#### Другое
- MC_Panel_API.postman_collection.json - Postman коллекция (40+ запросов)
- .gitignore - Git ignore правила
- .dockerignore - Docker ignore правила
- .env.example - Шаблон переменных окружения
### Исправлено 🐛
- Ошибка импорта `authlib.integrations.fastapi_client``starlette_client`
- Проблемы с drag & drop в файловом менеджере (отключено по запросу)
- Уведомления теперь показываются для всех действий
- Real-time обновления в тикетах работают корректно
### Безопасность 🔒
- JWT токены с истечением через 7 дней
- Bcrypt хеширование паролей (cost factor 12)
- OpenID Connect поддержка
- Проверка прав доступа на всех эндпоинтах
- Защита файловой системы от path traversal
- Non-root Docker пользователь
- Security headers в Nginx
- Rate limiting в Nginx
- HTTPS обязательный для production
---
## Типы изменений
- **Добавлено** ✨ - новая функциональность
- **Изменено** 🔄 - изменения в существующей функциональности
- **Устарело** ⚠️ - функциональность, которая скоро будет удалена
- **Удалено** 🗑️ - удалённая функциональность
- **Исправлено** 🐛 - исправление ошибок
- **Безопасность** 🔒 - изменения, связанные с безопасностью
---
## Ссылки
- [Документация](ДОКУМЕНТАЦИЯ.md)
- [API](API.md)
- [Docker](DOCKER.md)
- [FAQ](FAQ.md)
---
**Формат:** [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/)
**Версионирование:** [Semantic Versioning](https://semver.org/lang/ru/)

191
DOCKER_SEPARATE_README.md Normal file
View File

@@ -0,0 +1,191 @@
# MC Panel - Separate Docker Services
## Обзор
Теперь у вас есть отдельные Dockerfile для backend и frontend, что обеспечивает:
- **Лучшую изоляцию** - каждый сервис в своем контейнере
- **Независимое масштабирование** - можно масштабировать backend и frontend отдельно
- **Гибкость деплоя** - можно деплоить сервисы на разные серверы
- **Оптимизацию ресурсов** - каждый контейнер оптимизирован под свою задачу
## Структура файлов
```
mc-panel/
├── backend/
│ └── Dockerfile # Backend (FastAPI + Python)
├── frontend/
│ └── Dockerfile # Frontend (React + Nginx)
├── docker-compose.separate.yml # Production с отдельными сервисами
├── docker-compose.dev.yml # Development с hot reload
├── start-separate.bat # Запуск production
└── start-dev.bat # Запуск development
```
## Backend Dockerfile
**Особенности:**
- Базовый образ: `python:3.11-slim`
- Пользователь: `mcpanel` (UID/GID 1000)
- Порт: 8000
- Health check: `/health` endpoint
- Volumes: `/app/servers`, `/app/data`, `/app/logs`
- Init процесс: `tini`
**Команда сборки:**
```bash
cd backend
docker build -t mc-panel-backend .
```
## Frontend Dockerfile
**Multi-stage сборка:**
### Stage 1: Builder
- Базовый образ: `node:20-alpine`
- Собирает React приложение
- Оптимизирует статические файлы
### Stage 2: Production
- Базовый образ: `nginx:alpine`
- Служит статические файлы
- Проксирует API запросы к backend
- Поддерживает React Router (SPA)
### Stage 3: Development
- Базовый образ: `node:20-alpine`
- Vite dev server с hot reload
- Порт: 5173
**Команда сборки:**
```bash
cd frontend
# Production
docker build --target production -t mc-panel-frontend .
# Development
docker build --target development -t mc-panel-frontend-dev .
```
## Способы запуска
### 1. Production (отдельные сервисы)
```bash
# Windows
start-separate.bat
# Linux/macOS
docker-compose -f docker-compose.separate.yml up --build -d
```
**Доступ:**
- Frontend: http://localhost
- Backend API: http://localhost:8000
### 2. Development (с hot reload)
```bash
# Windows
start-dev.bat
# Linux/macOS
docker-compose -f docker-compose.dev.yml up --build -d
```
**Доступ:**
- Frontend Dev: http://localhost:5173
- Backend API: http://localhost:8000
### 3. Оригинальный (монолитный)
```bash
# Windows
docker-start.bat
# Linux/macOS
docker-compose up --build -d
```
## Конфигурация Nginx (Frontend)
Frontend Dockerfile включает оптимизированную конфигурацию Nginx:
- **Gzip сжатие** для статических файлов
- **Кеширование** статических ресурсов (1 год)
- **Security headers** (XSS, CSRF защита)
- **SPA поддержка** (React Router)
- **API прокси** на backend:8000
- **WebSocket прокси** для real-time функций
## Переменные окружения
### Backend
```env
PORT=8000
WORKERS=2
PYTHONPATH=/app
DEBUG=false
LOG_LEVEL=INFO
```
### Frontend (Development)
```env
VITE_API_URL=http://localhost:8000
```
## Volumes
### Production
- `mc_servers` - серверы Minecraft
- `mc_data` - данные приложения
- `mc_logs` - логи
### Development
- `mc_servers_dev` - серверы (dev)
- `mc_data_dev` - данные (dev)
- `mc_logs_dev` - логи (dev)
## Полезные команды
### Логи
```bash
# Production
docker-compose -f docker-compose.separate.yml logs -f backend
docker-compose -f docker-compose.separate.yml logs -f frontend
# Development
docker-compose -f docker-compose.dev.yml logs -f backend-dev
docker-compose -f docker-compose.dev.yml logs -f frontend-dev
```
### Остановка
```bash
# Production
docker-compose -f docker-compose.separate.yml down
# Development
docker-compose -f docker-compose.dev.yml down
```
### Пересборка
```bash
# Production
docker-compose -f docker-compose.separate.yml build --no-cache
# Development
docker-compose -f docker-compose.dev.yml build --no-cache
```
## Преимущества отдельных сервисов
1. **Масштабирование**: можно запустить несколько backend инстансов
2. **Обновления**: можно обновлять frontend и backend независимо
3. **Мониторинг**: отдельные метрики для каждого сервиса
4. **Безопасность**: изоляция сервисов
5. **Разработка**: разные команды могут работать независимо
## Рекомендации
- **Для разработки**: используйте `docker-compose.dev.yml`
- **Для тестирования**: используйте `docker-compose.separate.yml`
- **Для production**: рассмотрите Kubernetes или Docker Swarm
- **Для CI/CD**: можно собирать образы отдельно и деплоить независимо

View File

@@ -1,63 +0,0 @@
# Multi-stage build для MC Panel
# Stage 1: Build Frontend
FROM node:18-alpine AS frontend-builder
WORKDIR /app/frontend
# Копируем package файлы
COPY frontend/package*.json ./
# Устанавливаем зависимости
RUN npm ci --only=production
# Копируем исходники фронтенда
COPY frontend/ ./
# Собираем фронтенд
RUN npm run build
# Stage 2: Backend + Frontend
FROM python:3.11-slim
# Устанавливаем системные зависимости
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Создаем рабочую директорию
WORKDIR /app
# Копируем requirements и устанавливаем Python зависимости
COPY backend/requirements.txt ./backend/
RUN pip install --no-cache-dir -r backend/requirements.txt
# Копируем backend
COPY backend/ ./backend/
# Копируем собранный frontend из первого stage
COPY --from=frontend-builder /app/frontend/dist ./frontend/dist
# Создаем необходимые директории
RUN mkdir -p /app/backend/servers
# Создаем пользователя для запуска приложения
RUN useradd -m -u 1000 mcpanel && \
chown -R mcpanel:mcpanel /app
USER mcpanel
# Переменные окружения
ENV PYTHONUNBUFFERED=1
ENV PORT=8000
# Открываем порт
EXPOSE 8000
# Healthcheck
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/api/auth/oidc/providers || exit 1
# Запускаем приложение
WORKDIR /app/backend
CMD ["python", "main.py"]

View File

@@ -1,134 +0,0 @@
# 🔧 Исправление ошибки BaseModel
**Ошибка:** `NameError: name 'BaseModel' is not defined`
**Статус:** ИСПРАВЛЕНО ✅
---
## 🐛 Проблема
При запуске `backend/main.py` возникала ошибка:
```
Traceback (most recent call last):
File "backend\main.py", line 1655, in <module>
class RoleChange(BaseModel):
^^^^^^^^^
NameError: name 'BaseModel' is not defined
```
---
## ✅ Решение
Добавлен импорт `BaseModel` из `pydantic` в начало файла `backend/main.py`:
```python
from pydantic import BaseModel
```
---
## 🔧 Что было сделано
### Изменение в backend/main.py
**Было:**
```python
from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import asyncio
```
**Стало:**
```python
from fastapi import FastAPI, WebSocket, UploadFile, File, HTTPException, Depends, status, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel # ← Добавлено
import asyncio
```
---
## 🚀 Запуск
Теперь можно запустить панель:
```bash
cd backend
python main.py
```
Или используйте:
```bash
RESTART_ALL.bat
```
---
## ✅ Проверка
После запуска вы должны увидеть:
```
INFO: Started server process [PID]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000
```
Без ошибок! ✅
---
## 📚 Что такое BaseModel?
`BaseModel` - это базовый класс из библиотеки `pydantic`, который используется для создания моделей данных с валидацией.
**Используется для:**
- Валидация входных данных API
- Автоматическая генерация документации
- Сериализация/десериализация JSON
**Примеры в коде:**
```python
class RoleChange(BaseModel):
role: str
class BanRequest(BaseModel):
reason: str = "Заблокирован администратором"
class ServerAccess(BaseModel):
server_name: str
class PermissionsUpdate(BaseModel):
permissions: dict
```
---
## 🔍 Почему возникла ошибка?
При добавлении новых эндпоинтов управления пользователями были созданы новые модели данных (`RoleChange`, `BanRequest`, и т.д.), но импорт `BaseModel` не был добавлен.
---
## ✨ Готово!
Ошибка исправлена, панель должна запускаться без проблем.
**Запустите:**
```bash
cd backend
python main.py
```
---
**Дата исправления:** 15 января 2026
**Статус:** РАБОТАЕТ ✅

121
LINUX_DOCKER_FIX.md Normal file
View File

@@ -0,0 +1,121 @@
# Исправление Docker на Linux
## Проблема
Nginx не может запуститься из-за отсутствия секции "events" в конфигурации.
Backend работает отлично!
## ✅ ПРОСТОЕ РЕШЕНИЕ - Запуск без nginx
### Вариант 1: Быстрое исправление
```bash
# 1. Остановить все контейнеры
docker compose down
# 2. Создать папку data с файлами
mkdir -p data
cat > data/users.json << 'EOF'
{"admin":{"username":"admin","password":"$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG","role":"owner","servers":[],"permissions":{"manage_users":true,"manage_roles":true,"manage_servers":true,"manage_tickets":true,"manage_files":true,"delete_users":true,"view_all_resources":true},"resource_access":{"servers":[],"tickets":[],"files":[]}}}
EOF
echo '{}' > data/tickets.json
echo '{}' > data/daemons.json
# 3. Изменить порты в docker-compose.yml
sed -i 's/"8000:8000"/"80:8000"/' docker-compose.yml
# 4. Запустить только mc-panel
docker compose up -d mc-panel
```
### Вариант 2: Упрощенный docker-compose (РЕКОМЕНДУЕТСЯ)
1. **Скопируйте файлы**:
- `docker-compose-linux.yml`
- `backend/daemons.py` (обновленный)
2. **Выполните команды**:
```bash
# Остановить контейнеры
docker compose down
# Создать папку data
mkdir -p data
cat > data/users.json << 'EOF'
{"admin":{"username":"admin","password":"$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG","role":"owner","servers":[],"permissions":{"manage_users":true,"manage_roles":true,"manage_servers":true,"manage_tickets":true,"manage_files":true,"delete_users":true,"view_all_resources":true},"resource_access":{"servers":[],"tickets":[],"files":[]}}}
EOF
echo '{}' > data/tickets.json
echo '{}' > data/daemons.json
# Запустить с новой конфигурацией
docker compose -f docker-compose-linux.yml up --build -d
```
## Проверка
```bash
# Статус контейнера
docker compose ps
# Логи
docker compose logs mc-panel
# Проверка API
curl http://localhost/api/auth/oidc/providers
# Проверка в браузере
# Откройте http://IP_СЕРВЕРА
```
## Результат
**Панель доступна на IP сервера через порт 80**
**Логин: admin, пароль: Admin**
**SSO работает**
**Никаких проблем с nginx**
## Если нужен nginx позже
После того как панель заработает, можно настроить nginx отдельно:
```bash
# Создать правильную nginx конфигурацию
cat > nginx/simple.conf << 'EOF'
events {
worker_connections 1024;
}
http {
upstream mc_panel {
server mc-panel:8000;
}
server {
listen 80;
location / {
proxy_pass http://mc_panel;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
EOF
# Добавить nginx в docker-compose
# И изменить порты mc-panel обратно на "8000:8000"
```
## Структура файлов
```
📁 Проект
├── docker-compose-linux.yml # ✅ Упрощенная конфигурация
├── backend/daemons.py # ✅ Исправленные пути
└── data/ # ✅ Создается автоматически
├── users.json
├── tickets.json
└── daemons.json
```
---
**Используйте Вариант 2 - самый надежный способ!**

168
NGINX_SETUP.md Normal file
View File

@@ -0,0 +1,168 @@
# Настройка с Nginx + Frontend
## Архитектура
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Browser │───▶│ Nginx │───▶│ MC Panel │
│ │ │ │ │ (API only) │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────┐
│ Frontend │
│ (Static) │
└─────────────┘
```
- **Nginx**: Раздает статические файлы frontend + проксирует API запросы
- **MC Panel**: Только API backend (порт 8000, внутренний)
- **Frontend**: Собирается в Docker и копируется в nginx
## Файлы
### 1. ✅ Dockerfile
Уже настроен для многоэтапной сборки:
- Stage 1: Собирает frontend (`npm run build`)
- Stage 2: Python backend + статические файлы
### 2. ✅ nginx/default.conf
Полная nginx конфигурация:
- Раздача статических файлов из `/usr/share/nginx/html`
- Проксирование `/api/*` на backend
- WebSocket поддержка для `/ws/*`
- Gzip сжатие
- Кэширование статики
### 3. ✅ docker-compose-nginx.yml
- `mc-panel`: Backend API (внутренний порт 8000)
- `nginx`: Статика + reverse proxy (порт 80)
- `frontend-init`: Init контейнер для копирования статики
## Запуск
### Подготовка
```bash
# 1. Создать папку data
mkdir -p data
cat > data/users.json << 'EOF'
{"admin":{"username":"admin","password":"$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG","role":"owner","servers":[],"permissions":{"manage_users":true,"manage_roles":true,"manage_servers":true,"manage_tickets":true,"manage_files":true,"delete_users":true,"view_all_resources":true},"resource_access":{"servers":[],"tickets":[],"files":[]}}}
EOF
echo '{}' > data/tickets.json
echo '{}' > data/daemons.json
# 2. Скопировать файлы:
# - docker-compose-nginx.yml
# - nginx/default.conf (обновленный)
# - backend/daemons.py (обновленный)
```
### Запуск
```bash
# Остановить старые контейнеры
docker compose down
# Запустить с nginx
docker compose -f docker-compose-nginx.yml up --build -d
```
### Проверка
```bash
# Статус контейнеров
docker compose -f docker-compose-nginx.yml ps
# Логи
docker compose -f docker-compose-nginx.yml logs nginx
docker compose -f docker-compose-nginx.yml logs mc-panel
# Проверка API
curl http://localhost/api/auth/oidc/providers
# Проверка frontend
curl http://localhost/
```
## Что происходит
1. **Сборка**:
- Frontend собирается в `/app/frontend/dist`
- Backend копируется в `/app/backend`
2. **Инициализация**:
- `frontend-init` копирует статику в volume `frontend-static`
- Завершается после копирования
3. **Nginx**:
- Монтирует volume `frontend-static` в `/usr/share/nginx/html`
- Раздает статические файлы
- Проксирует `/api/*` на `mc-panel:8000`
4. **Backend**:
- Запускается на внутреннем порту 8000
- Доступен только через nginx
## Преимущества
**Производительность**: Nginx раздает статику быстрее
**Кэширование**: Статические файлы кэшируются
**Сжатие**: Gzip для всех файлов
**Безопасность**: Backend недоступен извне
**Масштабируемость**: Можно добавить SSL, балансировку
## Отладка
### Nginx не запускается
```bash
# Проверить конфигурацию
docker compose -f docker-compose-nginx.yml exec nginx nginx -t
# Проверить файлы
docker compose -f docker-compose-nginx.yml exec nginx ls -la /usr/share/nginx/html
```
### Frontend не загружается
```bash
# Проверить статические файлы
docker compose -f docker-compose-nginx.yml exec nginx ls -la /usr/share/nginx/html
# Проверить логи init контейнера
docker compose -f docker-compose-nginx.yml logs frontend-init
```
### API не работает
```bash
# Проверить backend
docker compose -f docker-compose-nginx.yml exec mc-panel curl http://localhost:8000/api/auth/oidc/providers
# Проверить проксирование
curl -v http://localhost/api/auth/oidc/providers
```
## Альтернативные варианты
### Вариант 1: Простой (без nginx)
Используйте `docker-compose-linux.yml` - backend раздает всё сам
### Вариант 2: Nginx + внешняя сборка
Соберите frontend локально и монтируйте папку `dist`
### Вариант 3: Отдельные контейнеры
Frontend и backend в разных контейнерах
## Результат
После успешного запуска:
- **Frontend**: `http://IP_СЕРВЕРА/`
- **API**: `http://IP_СЕРВЕРА/api/`
- **WebSocket**: `ws://IP_СЕРВЕРА/ws/`
- **Логин**: `admin` / `Admin`
---
**Nginx + Frontend в статике = максимальная производительность!** 🚀

View File

@@ -1,733 +0,0 @@
# 👑 Роль Владельца и Система Управления Правами
**Дата:** 15 января 2026
**Версия:** 1.1.0
---
## 📋 Содержание
1. [Обзор](#обзор)
2. [Роль Владельца](#роль-владельца)
3. [Система Прав](#система-прав)
4. [API Эндпоинты](#api-эндпоинты)
5. [Примеры Использования](#примеры-использования)
6. [Миграция Существующих Пользователей](#миграция-существующих-пользователей)
---
## Обзор
В MC Panel добавлена роль **Владелец (Owner)** с расширенными возможностями управления правами пользователей. Владелец может:
- ✅ Изменять роли пользователей (admin, user, support, banned)
- ✅ Управлять правами доступа к ресурсам
- ✅ Забирать доступ к серверам, тикетам, файлам
- ✅ Выдавать доступ к ресурсам
- ✅ Удалять пользователей
- ✅ Полный контроль над панелью
---
## Роль Владельца
### Как стать владельцем?
**Первый зарегистрированный пользователь автоматически получает роль владельца.**
```json
{
"username": "Root",
"role": "owner",
"permissions": {
"manage_users": true,
"manage_roles": true,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": true,
"view_all_resources": true
}
}
```
### Иерархия ролей
```
Owner (Владелец)
Admin (Администратор)
Support (Поддержка)
User (Пользователь)
Banned (Заблокирован)
```
### Возможности по ролям
| Возможность | Owner | Admin | Support | User | Banned |
|------------|-------|-------|---------|------|--------|
| Управление пользователями | ✅ | ✅ | ❌ | ❌ | ❌ |
| Изменение ролей | ✅ | ❌ | ❌ | ❌ | ❌ |
| Удаление пользователей | ✅ | ❌ | ❌ | ❌ | ❌ |
| Управление всеми серверами | ✅ | ✅ | ❌ | ❌ | ❌ |
| Управление своими серверами | ✅ | ✅ | ✅ | ✅ | ❌ |
| Просмотр всех тикетов | ✅ | ✅ | ✅ | ❌ | ❌ |
| Ответ на тикеты | ✅ | ✅ | ✅ | ✅ | ❌ |
| Создание тикетов | ✅ | ✅ | ✅ | ✅ | ❌ |
| Доступ к панели | ✅ | ✅ | ✅ | ✅ | ❌ |
---
## Система Прав
### Структура прав пользователя
```json
{
"username": "example_user",
"role": "user",
"permissions": {
"manage_users": false,
"manage_roles": false,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": false,
"view_all_resources": false
},
"resource_access": {
"servers": ["server1", "server2"],
"tickets": ["ticket1", "ticket2"],
"files": ["server1/*", "server2/*"]
}
}
```
### Типы прав
#### 1. manage_users
- Создание пользователей
- Редактирование профилей
- Просмотр списка пользователей
#### 2. manage_roles
- Изменение ролей пользователей
- Только для Owner
#### 3. manage_servers
- Создание серверов
- Запуск/остановка серверов
- Удаление серверов
#### 4. manage_tickets
- Создание тикетов
- Ответ на тикеты
- Изменение статуса
#### 5. manage_files
- Загрузка файлов
- Редактирование файлов
- Удаление файлов
#### 6. delete_users
- Удаление пользователей
- Только для Owner
#### 7. view_all_resources
- Просмотр всех серверов
- Просмотр всех тикетов
- Доступ ко всем файлам
---
## API Эндпоинты
### 1. Получить список пользователей
```http
GET /api/users
Authorization: Bearer <token>
```
**Требуется роль:** Owner или Admin
**Ответ:**
```json
{
"users": [
{
"id": 1,
"username": "Root",
"role": "owner",
"created_at": "2026-01-15T10:00:00Z"
},
{
"id": 2,
"username": "User1",
"role": "user",
"created_at": "2026-01-15T11:00:00Z"
}
]
}
```
---
### 2. Изменить роль пользователя
```http
PUT /api/users/{user_id}/role
Authorization: Bearer <token>
Content-Type: application/json
{
"role": "admin"
}
```
**Требуется роль:** Owner
**Доступные роли:**
- `owner` (только один владелец)
- `admin`
- `support`
- `user`
- `banned`
**Ответ:**
```json
{
"message": "Роль пользователя изменена",
"user": {
"id": 2,
"username": "User1",
"role": "admin"
}
}
```
---
### 3. Изменить права пользователя
```http
PUT /api/users/{user_id}/permissions
Authorization: Bearer <token>
Content-Type: application/json
{
"permissions": {
"manage_servers": true,
"manage_tickets": true,
"manage_files": false
}
}
```
**Требуется роль:** Owner
**Ответ:**
```json
{
"message": "Права пользователя обновлены",
"permissions": {
"manage_servers": true,
"manage_tickets": true,
"manage_files": false
}
}
```
---
### 4. Управление доступом к ресурсам
#### Выдать доступ к серверу
```http
POST /api/users/{user_id}/access/servers
Authorization: Bearer <token>
Content-Type: application/json
{
"server_name": "Survival"
}
```
**Требуется роль:** Owner или Admin
**Ответ:**
```json
{
"message": "Доступ к серверу выдан",
"server": "Survival",
"user": "User1"
}
```
#### Забрать доступ к серверу
```http
DELETE /api/users/{user_id}/access/servers/{server_name}
Authorization: Bearer <token>
```
**Требуется роль:** Owner или Admin
**Ответ:**
```json
{
"message": "Доступ к серверу отозван",
"server": "Survival",
"user": "User1"
}
```
---
### 5. Удалить пользователя
```http
DELETE /api/users/{user_id}
Authorization: Bearer <token>
```
**Требуется роль:** Owner
**Ответ:**
```json
{
"message": "Пользователь удалён",
"username": "User1"
}
```
**Примечание:** Владельца удалить нельзя!
---
### 6. Заблокировать пользователя
```http
POST /api/users/{user_id}/ban
Authorization: Bearer <token>
Content-Type: application/json
{
"reason": "Нарушение правил"
}
```
**Требуется роль:** Owner или Admin
**Ответ:**
```json
{
"message": "Пользователь заблокирован",
"username": "User1",
"reason": "Нарушение правил"
}
```
---
### 7. Разблокировать пользователя
```http
POST /api/users/{user_id}/unban
Authorization: Bearer <token>
```
**Требуется роль:** Owner или Admin
**Ответ:**
```json
{
"message": "Пользователь разблокирован",
"username": "User1"
}
```
---
## Примеры Использования
### Python
```python
import requests
# Токен владельца
token = "your_owner_token"
headers = {"Authorization": f"Bearer {token}"}
base_url = "http://localhost:8000"
# 1. Получить список пользователей
response = requests.get(f"{base_url}/api/users", headers=headers)
users = response.json()["users"]
print(f"Всего пользователей: {len(users)}")
# 2. Изменить роль пользователя
user_id = 2
response = requests.put(
f"{base_url}/api/users/{user_id}/role",
headers=headers,
json={"role": "admin"}
)
print(response.json()["message"])
# 3. Выдать доступ к серверу
response = requests.post(
f"{base_url}/api/users/{user_id}/access/servers",
headers=headers,
json={"server_name": "Survival"}
)
print(response.json()["message"])
# 4. Заблокировать пользователя
response = requests.post(
f"{base_url}/api/users/{user_id}/ban",
headers=headers,
json={"reason": "Нарушение правил"}
)
print(response.json()["message"])
# 5. Разблокировать пользователя
response = requests.post(
f"{base_url}/api/users/{user_id}/unban",
headers=headers
)
print(response.json()["message"])
# 6. Удалить пользователя
response = requests.delete(
f"{base_url}/api/users/{user_id}",
headers=headers
)
print(response.json()["message"])
```
---
### JavaScript
```javascript
const token = "your_owner_token";
const baseUrl = "http://localhost:8000";
// 1. Получить список пользователей
async function getUsers() {
const response = await fetch(`${baseUrl}/api/users`, {
headers: { "Authorization": `Bearer ${token}` }
});
const data = await response.json();
console.log(`Всего пользователей: ${data.users.length}`);
}
// 2. Изменить роль пользователя
async function changeRole(userId, role) {
const response = await fetch(`${baseUrl}/api/users/${userId}/role`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ role })
});
const data = await response.json();
console.log(data.message);
}
// 3. Выдать доступ к серверу
async function grantServerAccess(userId, serverName) {
const response = await fetch(`${baseUrl}/api/users/${userId}/access/servers`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ server_name: serverName })
});
const data = await response.json();
console.log(data.message);
}
// 4. Заблокировать пользователя
async function banUser(userId, reason) {
const response = await fetch(`${baseUrl}/api/users/${userId}/ban`, {
method: "POST",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ reason })
});
const data = await response.json();
console.log(data.message);
}
// Использование
getUsers();
changeRole(2, "admin");
grantServerAccess(2, "Survival");
banUser(2, "Нарушение правил");
```
---
### cURL
```bash
# Токен владельца
TOKEN="your_owner_token"
BASE_URL="http://localhost:8000"
# 1. Получить список пользователей
curl -X GET "$BASE_URL/api/users" \
-H "Authorization: Bearer $TOKEN"
# 2. Изменить роль пользователя
curl -X PUT "$BASE_URL/api/users/2/role" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
# 3. Выдать доступ к серверу
curl -X POST "$BASE_URL/api/users/2/access/servers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"server_name": "Survival"}'
# 4. Забрать доступ к серверу
curl -X DELETE "$BASE_URL/api/users/2/access/servers/Survival" \
-H "Authorization: Bearer $TOKEN"
# 5. Заблокировать пользователя
curl -X POST "$BASE_URL/api/users/2/ban" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason": "Нарушение правил"}'
# 6. Разблокировать пользователя
curl -X POST "$BASE_URL/api/users/2/unban" \
-H "Authorization: Bearer $TOKEN"
# 7. Удалить пользователя
curl -X DELETE "$BASE_URL/api/users/2" \
-H "Authorization: Bearer $TOKEN"
```
---
## Миграция Существующих Пользователей
### Автоматическая миграция
При первом запуске обновлённой версии панели:
1. Первый пользователь в `users.json` получает роль `owner`
2. Все пользователи с ролью `admin` остаются `admin`
3. Все остальные пользователи получают роль `user`
4. Всем пользователям добавляются права по умолчанию
### Скрипт миграции
```python
# backend/migrate_users.py
import json
from pathlib import Path
def migrate_users():
users_file = Path("users.json")
if not users_file.exists():
print("Файл users.json не найден")
return
with open(users_file, "r", encoding="utf-8") as f:
users = json.load(f)
if not users:
print("Нет пользователей для миграции")
return
# Первый пользователь = owner
users[0]["role"] = "owner"
users[0]["permissions"] = {
"manage_users": True,
"manage_roles": True,
"manage_servers": True,
"manage_tickets": True,
"manage_files": True,
"delete_users": True,
"view_all_resources": True
}
# Остальные пользователи
for user in users[1:]:
if "role" not in user:
user["role"] = "user"
if "permissions" not in user:
if user["role"] == "admin":
user["permissions"] = {
"manage_users": True,
"manage_roles": False,
"manage_servers": True,
"manage_tickets": True,
"manage_files": True,
"delete_users": False,
"view_all_resources": True
}
else:
user["permissions"] = {
"manage_users": False,
"manage_roles": False,
"manage_servers": True,
"manage_tickets": True,
"manage_files": True,
"delete_users": False,
"view_all_resources": False
}
if "resource_access" not in user:
user["resource_access"] = {
"servers": [],
"tickets": [],
"files": []
}
# Сохранить
with open(users_file, "w", encoding="utf-8") as f:
json.dump(users, f, indent=2, ensure_ascii=False)
print(f"Миграция завершена! Обновлено пользователей: {len(users)}")
print(f"Владелец: {users[0]['username']}")
if __name__ == "__main__":
migrate_users()
```
**Запуск миграции:**
```bash
cd backend
python migrate_users.py
```
---
## UI Компоненты
### Панель управления пользователями
В панели администратора добавлен раздел "Управление пользователями" (только для Owner):
**Возможности:**
- Просмотр списка всех пользователей
- Изменение ролей
- Управление правами
- Выдача/отзыв доступа к ресурсам
- Блокировка/разблокировка
- Удаление пользователей
**Компонент:** `frontend/src/components/UserManagement.jsx`
---
## Безопасность
### Проверка прав
Все эндпоинты управления пользователями защищены:
```python
def require_owner(current_user: dict):
if current_user["role"] != "owner":
raise HTTPException(
status_code=403,
detail="Требуется роль владельца"
)
def require_admin_or_owner(current_user: dict):
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(
status_code=403,
detail="Требуется роль администратора или владельца"
)
```
### Логирование действий
Все действия владельца логируются:
```python
# Пример лога
{
"timestamp": "2026-01-15T12:00:00Z",
"action": "change_role",
"owner": "Root",
"target_user": "User1",
"old_role": "user",
"new_role": "admin"
}
```
---
## FAQ
### Можно ли иметь несколько владельцев?
Нет, владелец может быть только один. Но владелец может назначить несколько администраторов.
### Что делать, если владелец потерял доступ?
Отредактируйте `backend/users.json` вручную и измените роль нужного пользователя на `owner`.
### Может ли владелец удалить сам себя?
Нет, владельца удалить нельзя. Сначала нужно передать роль владельца другому пользователю.
### Как передать роль владельца?
```bash
# Через API
curl -X PUT "http://localhost:8000/api/users/2/role" \
-H "Authorization: Bearer $OWNER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "owner"}'
```
При передаче роли владельца, текущий владелец автоматически становится администратором.
---
## Changelog
### Версия 1.1.0 (15 января 2026)
**Добавлено:**
- ✅ Роль владельца (Owner)
- ✅ Система прав и разрешений
- ✅ API для управления пользователями
- ✅ Управление доступом к ресурсам
- ✅ Блокировка/разблокировка пользователей
- ✅ Удаление пользователей
- ✅ UI компонент управления пользователями
- ✅ Скрипт миграции
- ✅ Логирование действий
**Изменено:**
- Первый пользователь теперь получает роль `owner` вместо `admin`
- Добавлена проверка прав для всех эндпоинтов
- Обновлена структура пользователя в `users.json`
---
**Версия:** 1.1.0
**Дата:** 15 января 2026
**Полный контроль над панелью!** 👑

View File

@@ -1,297 +0,0 @@
# ✅ UI Владельца готов!
**Дата:** 15 января 2026
**Статус:** ГОТОВО К ИСПОЛЬЗОВАНИЮ ✅
---
## 🎉 Что было сделано
### 1. Frontend
-**UserManagement.jsx** - Компонент управления пользователями
-**App.jsx** - Добавлена кнопка "Управление" (жёлтая с иконкой щита)
- ✅ Модальное окно с полным UI
### 2. Backend
-**8 новых API эндпоинтов** добавлены в `main.py`:
1. `GET /api/users` - Список пользователей
2. `PUT /api/users/{username}/role` - Изменить роль
3. `POST /api/users/{username}/ban` - Заблокировать
4. `POST /api/users/{username}/unban` - Разблокировать
5. `DELETE /api/users/{username}` - Удалить
6. `POST /api/users/{username}/access/servers` - Выдать доступ
7. `DELETE /api/users/{username}/access/servers/{name}` - Забрать доступ
8. `PUT /api/users/{username}/permissions` - Изменить права
### 3. Инструменты
-**RESTART_ALL.bat** - Быстрый перезапуск всех сервисов
---
## 🚀 Как запустить
### Вариант 1: Автоматический перезапуск
```bash
RESTART_ALL.bat
```
### Вариант 2: Вручную
**Backend:**
```bash
cd backend
python main.py
```
**Frontend:**
```bash
cd frontend
npm run dev
```
### Вариант 3: Docker
```bash
docker-compose restart
```
---
## 🎯 Как использовать
### Шаг 1: Войдите как владелец
- Логин: **Root**
- Пароль: **arkonsad123**
### Шаг 2: Найдите кнопку "Управление"
В верхней панели справа увидите **жёлтую кнопку** с иконкой щита и текстом "Управление"
### Шаг 3: Управляйте пользователями
В открывшемся окне вы увидите:
**Список пользователей:**
- MihailPrud (User)
- arkonsad (User)
- Root (Owner) - это вы!
**Для каждого пользователя (кроме себя):**
- 🔵 **Кнопка "Роль"** - Изменить роль (Owner, Admin, Support, User, Banned)
- 🟠 **Кнопка блокировки** - Заблокировать пользователя
- 🔴 **Кнопка удаления** - Удалить пользователя
---
## 🎨 Что вы увидите
### Карточка пользователя
```
┌─────────────────────────────────────────────────────┐
│ 👤 MihailPrud [🔵 Роль] [🟠] [🔴] │
│ Пользователь │
│ 🖥️ 2 серверов │
│ ✅ Управление серверами │
│ ✅ Управление тикетами │
│ ✅ Управление файлами │
└─────────────────────────────────────────────────────┘
```
### Модальное окно изменения роли
```
┌─────────────────────────────────┐
│ Изменить роль: MihailPrud │
├─────────────────────────────────┤
│ [👑 Владелец] │
│ Полный контроль над панелью │
│ │
│ [🛡️ Администратор] │
│ Управление без изменения │
│ ролей │
│ │
│ [💬 Поддержка] │
│ Работа с тикетами │
│ │
│ [✅ Пользователь] ← Текущая │
│ Базовые возможности │
│ │
│ [🚫 Заблокирован] │
│ Доступ заблокирован │
│ │
│ [Отмена] │
└─────────────────────────────────┘
```
---
## 💡 Примеры использования
### Сделать пользователя администратором
1. Нажмите "Управление"
2. Найдите пользователя (например, MihailPrud)
3. Нажмите кнопку "Роль"
4. Выберите "Администратор"
5. Готово! Пользователь теперь админ
### Заблокировать пользователя
1. Нажмите "Управление"
2. Найдите пользователя
3. Нажмите оранжевую кнопку (Ban)
4. Подтвердите
5. Пользователь заблокирован
### Разблокировать пользователя
1. Найдите заблокированного пользователя (помечен 🚫)
2. Нажмите зелёную кнопку (UserCheck)
3. Пользователь разблокирован
### Удалить пользователя
1. Нажмите красную кнопку (Trash)
2. Подтвердите удаление
3. Пользователь удалён навсегда
---
## 🔒 Ограничения безопасности
### Что НЕЛЬЗЯ сделать:
- ❌ Изменить свою роль
- ❌ Заблокировать себя
- ❌ Удалить себя
- ❌ Удалить владельца
- ❌ Заблокировать владельца
### Что МОЖНО:
- ✅ Изменить роль любого пользователя (кроме себя)
- ✅ Назначить нового владельца (вы станете админом)
- ✅ Заблокировать любого пользователя (кроме владельца)
- ✅ Удалить любого пользователя (кроме владельца)
---
## 🎯 Роли и их возможности
### 👑 Owner (Владелец)
- ✅ Управление пользователями
- ✅ Изменение ролей
- ✅ Удаление пользователей
- ✅ Управление серверами
- ✅ Просмотр всех ресурсов
-Все права
### 🛡️ Admin (Администратор)
- ✅ Управление пользователями
- ✅ Управление серверами
- ✅ Просмотр всех ресурсов
- ❌ Изменение ролей
- ❌ Удаление пользователей
### 💬 Support (Поддержка)
- ✅ Просмотр всех тикетов
- ✅ Ответ на тикеты
- ❌ Управление серверами
- ❌ Управление пользователями
### ✅ User (Пользователь)
- ✅ Управление своими серверами
- ✅ Создание тикетов
- ✅ Управление своими файлами
- ❌ Просмотр чужих ресурсов
### 🚫 Banned (Заблокирован)
- ❌ Нет доступа к панели
-Все права отозваны
---
## 🐛 Troubleshooting
### Не вижу кнопку "Управление"
**Причина:** Вы не владелец
**Решение:**
1. Проверьте что вошли как Root
2. Проверьте `backend/users.json` - у Root должна быть роль `owner`
3. Перезапустите панель
### Кнопка есть, но ничего не происходит
**Причина:** Backend не перезапущен
**Решение:**
```bash
RESTART_ALL.bat
```
### Ошибка "Требуется роль владельца"
**Причина:** В `users.json` роль не `owner`
**Решение:**
1. Откройте `backend/users.json`
2. Найдите пользователя Root
3. Убедитесь что `"role": "owner"`
4. Перезапустите backend
### Список пользователей пустой
**Причина:** API не работает
**Решение:**
1. Проверьте что backend запущен
2. Откройте консоль браузера (F12)
3. Проверьте ошибки
4. Перезапустите backend
---
## 📊 Статистика
### Добавлено в версии 1.1.0
- **Файлов:** 4
- **Строк кода:** ~800
- **API эндпоинтов:** 8
- **Ролей:** 5
- **Прав:** 7
### Всего в проекте
- **Файлов:** 75+
- **Строк кода:** ~10,300
- **Строк документации:** ~7,500
- **API эндпоинтов:** 45
- **Компонентов React:** 16
---
## 🎉 Готово!
Теперь у вас есть полноценная система управления пользователями!
**Запустите:**
```bash
RESTART_ALL.bat
```
**Войдите как Root и нажмите жёлтую кнопку "Управление"!**
---
**Версия:** 1.1.0
**Дата:** 15 января 2026
**Статус:** PRODUCTION READY ✅
**Полный контроль над панелью!** 👑🚀

View File

@@ -1,274 +0,0 @@
# 👑 Владелец видит все серверы
**Дата:** 15 января 2026
**Статус:** РЕАЛИЗОВАНО ✅
---
## 🎯 Что изменилось
### До изменения
**Проблема:** Владелец видел только серверы, к которым у него есть доступ в поле `servers`
```json
{
"Root": {
"role": "owner",
"servers": [] // Пустой список = нет серверов
}
}
```
**Результат:** Владелец не видел никаких серверов, даже будучи owner
---
### После изменения
**Решение:** Владелец и администратор видят ВСЕ серверы независимо от поля `servers`
```python
# Новая логика в backend/main.py
can_view_all = user.get("role") in ["owner", "admin"] or \
user.get("permissions", {}).get("view_all_resources", False)
if not can_view_all and server_dir.name not in user.get("servers", []):
continue # Пропускаем сервер только для обычных пользователей
```
**Результат:** Владелец видит все серверы в системе!
---
## 📊 Логика доступа к серверам
### Кто видит какие серверы
| Роль | Видит серверы | Логика |
|------|---------------|--------|
| **Owner** | ✅ ВСЕ серверы | `role == "owner"` |
| **Admin** | ✅ ВСЕ серверы | `role == "admin"` |
| **Support** | ❌ Только свои | Проверка `servers` |
| **User** | ❌ Только свои | Проверка `servers` |
| **Banned** | ❌ Ничего | Нет доступа |
### Примеры
#### Владелец (Root)
```json
{
"username": "Root",
"role": "owner",
"servers": []
}
```
**Видит:** test, nya, 123, sdfsdf (все серверы в системе)
#### Администратор
```json
{
"username": "Admin1",
"role": "admin",
"servers": []
}
```
**Видит:** test, nya, 123, sdfsdf (все серверы в системе)
#### Пользователь (MihailPrud)
```json
{
"username": "MihailPrud",
"role": "user",
"servers": ["test", "nya"]
}
```
**Видит:** test, nya (только свои серверы)
#### Пользователь (arkonsad)
```json
{
"username": "arkonsad",
"role": "user",
"servers": ["123", "sdfsdf"]
}
```
**Видит:** 123, sdfsdf (только свои серверы)
---
## 🎫 Логика доступа к тикетам
### Кто видит какие тикеты
| Роль | Видит тикеты | Логика |
|------|--------------|--------|
| **Owner** | ✅ ВСЕ тикеты | `role == "owner"` |
| **Admin** | ✅ ВСЕ тикеты | `role == "admin"` |
| **Support** | ✅ ВСЕ тикеты | `role == "support"` |
| **User** | ❌ Только свои | `author == username` |
| **Banned** | ❌ Ничего | Нет доступа |
**Примечание:** Логика для тикетов уже была правильной, изменений не требовалось.
---
## 🔐 Права доступа
### view_all_resources
Новое право `view_all_resources` определяет, может ли пользователь видеть все ресурсы:
```json
{
"permissions": {
"view_all_resources": true // Видит все серверы и тикеты
}
}
```
**Автоматически установлено для:**
- ✅ Owner - `true`
- ✅ Admin - `true`
- ❌ Support - `false`
- ❌ User - `false`
- ❌ Banned - `false`
---
## 🚀 Как проверить
### Шаг 1: Войдите как владелец
```
Логин: Root
Пароль: arkonsad123
```
### Шаг 2: Проверьте список серверов
Вы должны увидеть ВСЕ серверы:
- test (от MihailPrud)
- nya (от MihailPrud)
- 123 (от arkonsad)
- sdfsdf (от arkonsad)
### Шаг 3: Проверьте тикеты
Нажмите "Тикеты" - вы должны видеть все тикеты от всех пользователей.
---
## 💡 Дополнительные возможности
### Выдать доступ к серверу пользователю
Теперь владелец может выдать доступ к любому серверу:
1. Нажмите "Управление"
2. Найдите пользователя
3. Нажмите "Доступ к серверам" (если добавить эту кнопку)
4. Выберите сервер
5. Пользователь получит доступ
**API:**
```bash
POST /api/users/{username}/access/servers
{
"server_name": "test"
}
```
### Забрать доступ к серверу
```bash
DELETE /api/users/{username}/access/servers/test
```
---
## 🔧 Технические детали
### Изменённый код
**Файл:** `backend/main.py`
**Эндпоинт:** `GET /api/servers`
**Было:**
```python
if user["role"] != "admin" and server_dir.name not in user.get("servers", []):
continue
```
**Стало:**
```python
can_view_all = user.get("role") in ["owner", "admin"] or \
user.get("permissions", {}).get("view_all_resources", False)
if not can_view_all and server_dir.name not in user.get("servers", []):
continue
```
**Изменения:**
1. ✅ Добавлена проверка роли `owner`
2. ✅ Добавлена проверка права `view_all_resources`
3. ✅ Улучшено логирование (показывает роль пользователя)
---
## 📊 Сравнение
### До изменения
```
Root (owner) → servers: [] → Видит: 0 серверов ❌
Admin (admin) → servers: [] → Видит: 4 сервера ✅
User (user) → servers: ["test"] → Видит: 1 сервер ✅
```
### После изменения
```
Root (owner) → servers: [] → Видит: 4 сервера ✅
Admin (admin) → servers: [] → Видит: 4 сервера ✅
User (user) → servers: ["test"] → Видит: 1 сервер ✅
```
---
## 🎯 Итог
**Проблема решена!**
Теперь владелец (и администратор) видят все серверы в системе, независимо от поля `servers` в их профиле.
### Что работает:
- ✅ Владелец видит все серверы
- ✅ Администратор видит все серверы
- ✅ Владелец видит все тикеты
- ✅ Администратор видит все тикеты
- ✅ Support видит все тикеты
- ✅ Пользователи видят только свои ресурсы
### Перезапустите панель:
```bash
RESTART_ALL.bat
```
Или вручную:
```bash
cd backend
python main.py
```
---
**Версия:** 1.1.0
**Дата:** 15 января 2026
**Статус:** РАБОТАЕТ ✅
**Полный контроль над всеми серверами!** 👑🖥️

View File

@@ -7,7 +7,7 @@
## 📚 Документация ## 📚 Документация
### 🎉 [ПРОЕКТ_ЗАВЕРШЁН.md](ПРОЕКТ_ЗАВЕРШЁН.md) ### 🎉 ПРОЕКТ_ЗАВЕРШЁН
**Полный обзор проекта** **Полный обзор проекта**
Comprehensive overview всего проекта: Comprehensive overview всего проекта:
@@ -19,7 +19,7 @@ Comprehensive overview всего проекта:
**Начните отсюда для общего понимания!** 🌟 **Начните отсюда для общего понимания!** 🌟
### 📋 [ФИНАЛЬНЫЙ_СПИСОК.md](ФИНАЛЬНЫЙ_СПИСОК.md) ### 📋 ФИНАЛЬНЫЙ_СПИСОК
**Полный список всех файлов** **Полный список всех файлов**
Детальный список всех файлов проекта: Детальный список всех файлов проекта:
@@ -31,7 +31,7 @@ Comprehensive overview всего проекта:
**Полная карта проекта!** 🗺️ **Полная карта проекта!** 🗺️
### ✅ [CHECKLIST.md](CHECKLIST.md) ### ✅ CHECKLIST
**Финальный Checklist** **Финальный Checklist**
Проверка завершения всех работ: Проверка завершения всех работ:
@@ -43,7 +43,7 @@ Comprehensive overview всего проекта:
**Подтверждение готовности!** ✔️ **Подтверждение готовности!** ✔️
### 👑 [OWNER_PERMISSIONS.md](OWNER_PERMISSIONS.md) ### 👑 OWNER_PERMISSIONS
**Роль Владельца и Система Прав** **Роль Владельца и Система Прав**
Новая система управления пользователями: Новая система управления пользователями:
@@ -55,7 +55,7 @@ Comprehensive overview всего проекта:
**Полный контроль над панелью!** 🎯 **Полный контроль над панелью!** 🎯
### 🔧 [MIGRATION_FIX.md](MIGRATION_FIX.md) ### 🔧 MIGRATION_FIX
**Исправление миграции** **Исправление миграции**
Решение проблемы KeyError при миграции: Решение проблемы KeyError при миграции:
@@ -67,7 +67,7 @@ Comprehensive overview всего проекта:
**Миграция работает!** ✔️ **Миграция работает!** ✔️
### ✅ [OWNER_UI_READY.md](OWNER_UI_READY.md) ### ✅ OWNER_UI_READY
**UI Владельца готов!** **UI Владельца готов!**
Полная инструкция по использованию: Полная инструкция по использованию:
@@ -79,7 +79,7 @@ Comprehensive overview всего проекта:
**Управление пользователями работает!** 👑 **Управление пользователями работает!** 👑
### 👁️ [OWNER_VIEW_ALL.md](OWNER_VIEW_ALL.md) ### 👁️ OWNER_VIEW_ALL
**Владелец видит все серверы** **Владелец видит все серверы**
Изменение логики доступа: Изменение логики доступа:
@@ -91,7 +91,31 @@ Comprehensive overview всего проекта:
**Полный контроль над всеми ресурсами!** 🖥️ **Полный контроль над всеми ресурсами!** 🖥️
### 📝 [CHANGELOG.md](CHANGELOG.md) ### 👑 MULTIPLE_OWNERS
**Несколько владельцев**
Возможность назначить несколько владельцев:
- 🎯 Что изменилось
- 📊 Новая логика
- 💡 Примеры использования
- 🔒 Правила безопасности
- 🎯 Рекомендации
**Больше владельцев - больше контроля!** 👑👑
### 🚀 DRONE_SIMPLIFIED
**Упрощённый CI/CD**
Упрощение Drone конфигурации:
- 🎯 Что изменилось (4→2 пайплайна)
- 📋 Оставшиеся пайплайны
- 🗑️ Удалённые компоненты
- 🔧 Настройка
- ✅ Преимущества
**Меньше сложности - больше контроля!** 🔧
### 📝 CHANGELOG
**История изменений** **История изменений**
Все изменения проекта: Все изменения проекта:
@@ -102,7 +126,7 @@ Comprehensive overview всего проекта:
**Отслеживание изменений!** 📊 **Отслеживание изменений!** 📊
### 🎉 [VERSION_1.1.0.md](VERSION_1.1.0.md) ### 🎉 VERSION_1.1.0
**Релиз v1.1.0** **Релиз v1.1.0**
Что нового в версии 1.1.0: Что нового в версии 1.1.0:
@@ -116,7 +140,7 @@ Comprehensive overview всего проекта:
--- ---
### 📖 [ДОКУМЕНТАЦИЯ.md](ДОКУМЕНТАЦИЯ.md) ### 📖 ДОКУМЕНТАЦИЯ
**Полная документация проекта** **Полная документация проекта**
Содержит всю информацию о проекте: Содержит всю информацию о проекте:
@@ -137,7 +161,7 @@ Comprehensive overview всего проекта:
--- ---
### 🌐 [API.md](API.md) ### 🌐 API
**Документация API** **Документация API**
Полное описание REST API: Полное описание REST API:
@@ -154,7 +178,7 @@ Comprehensive overview всего проекта:
--- ---
### 📦 [MC_Panel_API.postman_collection.json](MC_Panel_API.postman_collection.json) ### 📦 MC_Panel_API.postman_collection
**Postman коллекция** **Postman коллекция**
Готовая коллекция для тестирования API: Готовая коллекция для тестирования API:
@@ -165,6 +189,8 @@ Comprehensive overview всего проекта:
**Импортируйте в Postman!** 📮 **Импортируйте в Postman!** 📮
**Всё .md файлы есть на вики**
### Вариант 1: Docker (рекомендуется) 🐳 ### Вариант 1: Docker (рекомендуется) 🐳
```bash ```bash
@@ -256,8 +282,6 @@ mc-panel/
│ │ ├── components/ # React компоненты │ │ ├── components/ # React компоненты
│ │ └── themes.js # Темы │ │ └── themes.js # Темы
│ └── package.json # npm зависимости │ └── package.json # npm зависимости
├── ДОКУМЕНТАЦИЯ.md # Документация проекта
├── API.md # API документация
├── MC_Panel_API.postman_collection.json # Postman ├── MC_Panel_API.postman_collection.json # Postman
└── README.md # Этот файл └── README.md # Этот файл
``` ```
@@ -282,15 +306,9 @@ mc-panel/
## 📞 Поддержка ## 📞 Поддержка
- **Документация:** [ДОКУМЕНТАЦИЯ.md](ДОКУМЕНТАЦИЯ.md) - **Документация**
- **API:** [API.md](API.md) - **API**
- **Тикеты:** Используйте систему тикетов в панели - **Тикеты:**
---
## 📝 Лицензия
AGPL-3.0 License
--- ---
@@ -305,7 +323,7 @@ AGPL-3.0 License
--- ---
**Версия:** 1.0.0 **Версия:** 1.1.0
**Дата:** 15 января 2026 **Дата:** 15 января 2026
**Приятного использования!** 🎮 **Приятного использования!** 🎮

View File

@@ -1,427 +0,0 @@
# 🎉 MC Panel v1.1.0 - Система прав и ролей
**Дата релиза:** 15 января 2026
**Тип релиза:** Minor Update
**Статус:** RELEASED ✅
---
## 📋 Что нового?
### 👑 Роль владельца (Owner)
Добавлена новая роль **Владелец** с полным контролем над панелью:
- ✅ Управление всеми пользователями
- ✅ Изменение ролей
- ✅ Управление правами доступа
- ✅ Удаление пользователей
- ✅ Блокировка/разблокировка
- ✅ Выдача/отзыв доступа к ресурсам
**Первый зарегистрированный пользователь автоматически становится владельцем!**
---
### 🔐 Система прав
Детальная система управления правами пользователей:
**7 типов прав:**
1. `manage_users` - Управление пользователями
2. `manage_roles` - Изменение ролей (только Owner)
3. `manage_servers` - Управление серверами
4. `manage_tickets` - Управление тикетами
5. `manage_files` - Управление файлами
6. `delete_users` - Удаление пользователей (только Owner)
7. `view_all_resources` - Просмотр всех ресурсов
---
### 👥 5 ролей пользователей
```
Owner (Владелец) - Полный контроль
Admin (Администратор) - Управление панелью
Support (Поддержка) - Работа с тикетами
User (Пользователь) - Базовые возможности
Banned (Заблокирован) - Нет доступа
```
---
### 🆕 Новые API эндпоинты
#### 1. Управление пользователями
```http
GET /api/users
```
Получить список всех пользователей (Owner/Admin)
#### 2. Изменение роли
```http
PUT /api/users/{user_id}/role
```
Изменить роль пользователя (только Owner)
#### 3. Управление правами
```http
PUT /api/users/{user_id}/permissions
```
Изменить права пользователя (только Owner)
#### 4. Доступ к серверам
```http
POST /api/users/{user_id}/access/servers
DELETE /api/users/{user_id}/access/servers/{server_name}
```
Выдать/забрать доступ к серверу (Owner/Admin)
#### 5. Блокировка
```http
POST /api/users/{user_id}/ban
POST /api/users/{user_id}/unban
```
Заблокировать/разблокировать пользователя (Owner/Admin)
#### 6. Удаление
```http
DELETE /api/users/{user_id}
```
Удалить пользователя (только Owner)
---
### 🛠️ Инструменты миграции
#### migrate_users.py
Автоматический скрипт миграции существующих пользователей:
```bash
cd backend
python migrate_users.py
```
**Что делает:**
- ✅ Создаёт backup users.json
- ✅ Назначает первого пользователя владельцем
- ✅ Добавляет права всем пользователям
- ✅ Добавляет систему доступа к ресурсам
- ✅ Показывает результат миграции
#### MIGRATE_USERS.bat
Bat файл для Windows:
```bash
MIGRATE_USERS.bat
```
---
### 📚 Новая документация
#### OWNER_PERMISSIONS.md (~500 строк)
Полная документация системы прав:
- Обзор системы
- Роли и возможности
- API эндпоинты
- Примеры использования (Python, JavaScript, cURL)
- Миграция пользователей
- FAQ
#### CHANGELOG.md
История всех изменений проекта:
- Версия 1.1.0 - Система прав
- Версия 1.0.0 - Первый релиз
---
## 🔄 Изменения
### Backend
**Структура пользователя:**
```json
{
"username": "example",
"role": "user",
"permissions": {
"manage_users": false,
"manage_roles": false,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": false,
"view_all_resources": false
},
"resource_access": {
"servers": ["server1"],
"tickets": ["ticket1"],
"files": ["server1/*"]
}
}
```
**Проверка прав:**
```python
def require_owner(current_user: dict):
if current_user["role"] != "owner":
raise HTTPException(status_code=403)
def require_admin_or_owner(current_user: dict):
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(status_code=403)
```
---
## 📊 Сравнение версий
| Функция | v1.0.0 | v1.1.0 |
|---------|--------|--------|
| Роли | 2 (admin, user) | 5 (owner, admin, support, user, banned) |
| Система прав | ❌ | ✅ 7 типов прав |
| Управление пользователями | Базовое | Расширенное |
| Блокировка пользователей | ❌ | ✅ |
| Удаление пользователей | ❌ | ✅ (только Owner) |
| Доступ к ресурсам | Все или ничего | Детальный контроль |
| API эндпоинтов | 37 | 45 (+8) |
---
## 🚀 Обновление с v1.0.0
### Шаг 1: Backup
```bash
# Создайте backup
BACKUP_DATA.bat
```
### Шаг 2: Обновление кода
```bash
# Остановите панель
STOP_DOCKER.bat
# Обновите код
git pull origin main
# Или скачайте новую версию
```
### Шаг 3: Миграция пользователей
```bash
# Запустите миграцию
MIGRATE_USERS.bat
# Или вручную
cd backend
python migrate_users.py
```
### Шаг 4: Перезапуск
```bash
# Запустите панель
START_DOCKER.bat
# Или
docker-compose up -d --build
```
### Шаг 5: Проверка
1. Войдите как владелец
2. Проверьте права пользователей
3. Настройте доступ к ресурсам
---
## 💡 Примеры использования
### Python
```python
import requests
token = "owner_token"
headers = {"Authorization": f"Bearer {token}"}
base_url = "http://localhost:8000"
# Получить пользователей
users = requests.get(f"{base_url}/api/users", headers=headers).json()
# Изменить роль
requests.put(
f"{base_url}/api/users/2/role",
headers=headers,
json={"role": "admin"}
)
# Выдать доступ к серверу
requests.post(
f"{base_url}/api/users/2/access/servers",
headers=headers,
json={"server_name": "Survival"}
)
# Заблокировать пользователя
requests.post(
f"{base_url}/api/users/2/ban",
headers=headers,
json={"reason": "Нарушение правил"}
)
```
### JavaScript
```javascript
const token = "owner_token";
const baseUrl = "http://localhost:8000";
// Получить пользователей
const users = await fetch(`${baseUrl}/api/users`, {
headers: { "Authorization": `Bearer ${token}` }
}).then(r => r.json());
// Изменить роль
await fetch(`${baseUrl}/api/users/2/role`, {
method: "PUT",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ role: "admin" })
});
```
---
## 🔒 Безопасность
### Новые меры безопасности
- ✅ Проверка прав на всех административных эндпоинтах
- ✅ Логирование действий владельца
- ✅ Защита от удаления владельца
- ✅ Автоматическое понижение роли при передаче прав
- ✅ Детальный контроль доступа к ресурсам
### Рекомендации
1. Регулярно проверяйте права пользователей
2. Используйте роль Support для службы поддержки
3. Блокируйте неактивных пользователей
4. Логируйте все административные действия
5. Создавайте backup перед изменением прав
---
## 📝 Миграция данных
### Автоматическая миграция
При запуске `migrate_users.py`:
1. ✅ Создаётся backup с timestamp
2. ✅ Первый пользователь → Owner
3. ✅ Admin остаются Admin
4. ✅ Остальные → User
5. ✅ Всем добавляются права
6. ✅ Добавляется система доступа к ресурсам
### Ручная миграция
Если нужно вручную:
```json
{
"username": "Root",
"password": "hashed_password",
"role": "owner",
"permissions": {
"manage_users": true,
"manage_roles": true,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": true,
"view_all_resources": true
},
"resource_access": {
"servers": [],
"tickets": [],
"files": []
}
}
```
---
## 🐛 Известные проблемы
### Нет критических проблем
Все функции протестированы и работают корректно.
### Ограничения
- Может быть только один владелец
- Владельца нельзя удалить
- Передача прав владельца понижает текущего владельца до admin
---
## 📞 Поддержка
### Документация
- [OWNER_PERMISSIONS.md](OWNER_PERMISSIONS.md) - Система прав
- [CHANGELOG.md](CHANGELOG.md) - История изменений
- [API.md](API.md) - API документация
- [FAQ.md](FAQ.md) - Часто задаваемые вопросы
### Проблемы с миграцией?
1. Проверьте backup файл
2. Читайте вывод скрипта миграции
3. Проверьте формат users.json
4. Восстановите из backup если нужно
---
## 🎯 Что дальше?
### Планы на v1.2.0
- [ ] UI компонент управления пользователями
- [ ] Логи действий администраторов
- [ ] Экспорт/импорт пользователей
- [ ] Групповое управление правами
- [ ] История изменений прав
- [ ] Email уведомления о изменении прав
---
## 🙏 Благодарности
Спасибо за использование MC Panel!
Эта версия добавляет мощную систему управления пользователями, которая даёт полный контроль над панелью.
---
**Версия:** 1.1.0
**Дата:** 15 января 2026
**Статус:** RELEASED ✅
**Полный контроль над панелью!** 👑🚀

79
backend/Dockerfile Normal file
View File

@@ -0,0 +1,79 @@
# ================================
# MC Panel Backend - Production Dockerfile
# ================================
FROM python:3.11-slim AS production
# Метаданные
LABEL maintainer="MC Panel Team" \
version="2.0.0" \
description="MC Panel Backend - FastAPI Server" \
component="backend"
# Переменные окружения
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/app \
PORT=8000 \
WORKERS=1 \
DEBIAN_FRONTEND=noninteractive
# Устанавливаем системные зависимости
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
procps \
ca-certificates \
tini \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Создаем пользователя для безопасности
RUN groupadd -r -g 1000 mcpanel && \
useradd -r -u 1000 -g mcpanel -d /app -s /bin/bash mcpanel
# Создаем рабочую директорию
WORKDIR /app
# Копируем requirements и устанавливаем зависимости
COPY requirements.txt ./
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# Копируем исходный код
COPY --chown=mcpanel:mcpanel . ./
# Создаем необходимые директории
RUN mkdir -p \
servers \
data \
logs \
&& touch users.json tickets.json
# Создаем конфигурационные файлы по умолчанию если их нет
RUN [ ! -f users.json ] && echo '{}' > users.json || true && \
[ ! -f tickets.json ] && echo '{}' > tickets.json || true
# Устанавливаем права доступа
RUN chown -R mcpanel:mcpanel /app && \
chmod -R 755 /app && \
chmod +x main.py
# Переключаемся на непривилегированного пользователя
USER mcpanel
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:${PORT}/health 2>/dev/null || \
curl -f http://localhost:${PORT}/ 2>/dev/null || exit 1
# Expose порт
EXPOSE 8000
# Volumes для персистентных данных
VOLUME ["/app/servers", "/app/data", "/app/logs"]
# Используем tini как init процесс
ENTRYPOINT ["/usr/bin/tini", "--"]
# Команда запуска
CMD ["sh", "-c", "python -m uvicorn main:app --host 0.0.0.0 --port ${PORT} --workers ${WORKERS}"]

336
backend/daemons.py Normal file
View File

@@ -0,0 +1,336 @@
"""
Управление демонами (удаленными серверами)
"""
from fastapi import APIRouter, HTTPException, Depends, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from typing import List, Optional
import json
import httpx
from pathlib import Path
from jose import JWTError, jwt
router = APIRouter()
security = HTTPBearer(auto_error=False)
# Файл с конфигурацией демонов
DAEMONS_FILE = Path("data/daemons.json")
DAEMONS_FILE.parent.mkdir(exist_ok=True)
# Файл с пользователями - проверяем оба возможных пути
if Path("users.json").exists():
USERS_FILE = Path("users.json")
elif Path("backend/users.json").exists():
USERS_FILE = Path("backend/users.json")
else:
USERS_FILE = Path("users.json") # По умолчанию
# Настройки JWT (должны совпадать с main.py)
SECRET_KEY = "your-secret-key-change-this-in-production-12345"
ALGORITHM = "HS256"
def load_users_dict():
"""Загрузить пользователей из файла"""
if USERS_FILE.exists():
with open(USERS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
def get_current_user_from_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Получить текущего пользователя из токена"""
if not credentials:
raise HTTPException(status_code=401, detail="Требуется авторизация")
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Неверный токен")
# Пытаемся получить роль из токена
role = payload.get("role")
print(f"[DEBUG] Username from token: {username}")
print(f"[DEBUG] Role from token: {role}")
# Если роли нет в токене, загружаем из базы
if not role:
print(f"[DEBUG] Role not in token, loading from database...")
print(f"[DEBUG] USERS_FILE path: {USERS_FILE}")
print(f"[DEBUG] USERS_FILE exists: {USERS_FILE.exists()}")
users = load_users_dict()
print(f"[DEBUG] Loaded users: {list(users.keys())}")
if username not in users:
raise HTTPException(status_code=401, detail="Пользователь не найден")
role = users[username].get("role", "user")
print(f"[DEBUG] Role from database: {role}")
print(f"[DEBUG] Final role: {role}")
return {"username": username, "role": role}
except JWTError as e:
print(f"[DEBUG] JWT Error: {e}")
raise HTTPException(status_code=401, detail="Неверный токен")
class DaemonCreate(BaseModel):
name: str
address: str
port: int
key: str
remarks: Optional[str] = ""
class DaemonUpdate(BaseModel):
name: Optional[str] = None
address: Optional[str] = None
port: Optional[int] = None
key: Optional[str] = None
remarks: Optional[str] = None
def load_daemons():
"""Загрузить список демонов"""
if not DAEMONS_FILE.exists():
return {}
with open(DAEMONS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
def save_daemons(daemons: dict):
"""Сохранить список демонов"""
with open(DAEMONS_FILE, 'w', encoding='utf-8') as f:
json.dump(daemons, f, indent=2, ensure_ascii=False)
async def check_daemon_connection(address: str, port: int, key: str) -> dict:
"""Проверить подключение к демону"""
url = f"http://{address}:{port}/api/status"
headers = {"Authorization": f"Bearer {key}"}
print(f"[DEBUG] Checking daemon connection:")
print(f"[DEBUG] URL: {url}")
print(f"[DEBUG] Key: {key[:20]}...")
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(url, headers=headers)
print(f"[DEBUG] Status: {response.status_code}")
if response.status_code == 200:
data = response.json()
print(f"[DEBUG] Response: {data}")
return data
else:
print(f"[DEBUG] Error response: {response.text}")
raise HTTPException(status_code=400, detail=f"Failed to connect to daemon: {response.status_code}")
except httpx.RequestError as e:
print(f"[DEBUG] Connection error: {e}")
raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}")
@router.get("/api/daemons")
async def get_daemons(current_user: dict = Depends(get_current_user_from_token)):
"""Получить список всех демонов"""
# Только админы и владельцы могут видеть демоны
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(status_code=403, detail="Access denied")
daemons = load_daemons()
# Проверяем статус каждого демона
result = []
for daemon_id, daemon in daemons.items():
daemon_info = {
"id": daemon_id,
**daemon,
"status": "offline"
}
try:
# Пытаемся получить статус
status = await check_daemon_connection(
daemon["address"],
daemon["port"],
daemon["key"]
)
daemon_info["status"] = "online"
daemon_info["system"] = status.get("system", {})
daemon_info["servers"] = status.get("servers", {})
except:
pass
result.append(daemon_info)
return result
@router.post("/api/daemons")
async def create_daemon(
daemon: DaemonCreate,
current_user: dict = Depends(get_current_user_from_token)
):
"""Добавить новый демон"""
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(status_code=403, detail="Access denied")
# Проверяем подключение
await check_daemon_connection(daemon.address, daemon.port, daemon.key)
daemons = load_daemons()
# Генерируем ID
daemon_id = f"daemon-{len(daemons) + 1}"
daemons[daemon_id] = {
"name": daemon.name,
"address": daemon.address,
"port": daemon.port,
"key": daemon.key,
"remarks": daemon.remarks,
"created_at": str(Path().cwd()) # Временная метка
}
save_daemons(daemons)
return {
"success": True,
"daemon_id": daemon_id,
"message": "Daemon added successfully"
}
@router.get("/api/daemons/{daemon_id}")
async def get_daemon(
daemon_id: str,
current_user: dict = Depends(get_current_user_from_token)
):
"""Получить информацию о демоне"""
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(status_code=403, detail="Access denied")
daemons = load_daemons()
if daemon_id not in daemons:
raise HTTPException(status_code=404, detail="Daemon not found")
daemon = daemons[daemon_id]
# Получаем статус
try:
status = await check_daemon_connection(
daemon["address"],
daemon["port"],
daemon["key"]
)
daemon["status"] = "online"
daemon["system"] = status.get("system", {})
daemon["servers"] = status.get("servers", {})
except:
daemon["status"] = "offline"
return {
"id": daemon_id,
**daemon
}
@router.put("/api/daemons/{daemon_id}")
async def update_daemon(
daemon_id: str,
daemon_update: DaemonUpdate,
current_user: dict = Depends(get_current_user_from_token)
):
"""Обновить демон"""
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(status_code=403, detail="Access denied")
daemons = load_daemons()
if daemon_id not in daemons:
raise HTTPException(status_code=404, detail="Daemon not found")
# Обновляем поля
daemon = daemons[daemon_id]
if daemon_update.name:
daemon["name"] = daemon_update.name
if daemon_update.address:
daemon["address"] = daemon_update.address
if daemon_update.port:
daemon["port"] = daemon_update.port
if daemon_update.key:
daemon["key"] = daemon_update.key
if daemon_update.remarks is not None:
daemon["remarks"] = daemon_update.remarks
# Проверяем подключение с новыми данными
await check_daemon_connection(
daemon["address"],
daemon["port"],
daemon["key"]
)
save_daemons(daemons)
return {
"success": True,
"message": "Daemon updated successfully"
}
@router.delete("/api/daemons/{daemon_id}")
async def delete_daemon(
daemon_id: str,
current_user: dict = Depends(get_current_user_from_token)
):
"""Удалить демон"""
if current_user["role"] not in ["owner", "admin"]:
raise HTTPException(status_code=403, detail="Access denied")
daemons = load_daemons()
if daemon_id not in daemons:
raise HTTPException(status_code=404, detail="Daemon not found")
del daemons[daemon_id]
save_daemons(daemons)
return {
"success": True,
"message": "Daemon deleted successfully"
}
@router.get("/api/daemons/{daemon_id}/servers")
async def get_daemon_servers(
daemon_id: str,
current_user: dict = Depends(get_current_user_from_token)
):
"""Получить список серверов на демоне"""
daemons = load_daemons()
if daemon_id not in daemons:
raise HTTPException(status_code=404, detail="Daemon not found")
daemon = daemons[daemon_id]
url = f"http://{daemon['address']}:{daemon['port']}/api/servers"
headers = {"Authorization": f"Bearer {daemon['key']}"}
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(url, headers=headers)
if response.status_code == 200:
return response.json()
else:
raise HTTPException(status_code=400, detail="Failed to get servers from daemon")
except httpx.RequestError as e:
raise HTTPException(status_code=400, detail=f"Connection error: {str(e)}")

10
backend/data/daemons.json Normal file
View File

@@ -0,0 +1,10 @@
{
"daemon-1": {
"name": "Test",
"address": "127.0.0.1",
"port": 24444,
"key": "JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4",
"remarks": "",
"created_at": "D:\\Desktop\\adadad"
}
}

View File

@@ -332,7 +332,7 @@ async def register(data: dict):
save_users(users) save_users(users)
access_token = create_access_token(data={"sub": username}) access_token = create_access_token(data={"sub": username, "role": role})
return { return {
"access_token": access_token, "access_token": access_token,
"token_type": "bearer", "token_type": "bearer",
@@ -353,7 +353,7 @@ async def login(data: dict):
if not verify_password(password, user["password"]): if not verify_password(password, user["password"]):
raise HTTPException(401, "Неверное имя пользователя или пароль") raise HTTPException(401, "Неверное имя пользователя или пароль")
access_token = create_access_token(data={"sub": username}) access_token = create_access_token(data={"sub": username, "role": user["role"]})
return { return {
"access_token": access_token, "access_token": access_token,
"token_type": "bearer", "token_type": "bearer",
@@ -445,9 +445,11 @@ async def delete_user(username: str, user: dict = Depends(get_current_user)):
if username not in users: if username not in users:
raise HTTPException(404, "Пользователь не найден") raise HTTPException(404, "Пользователь не найден")
# Нельзя удалить другого владельца # Проверяем, что не удаляем последнего владельца
if users[username]["role"] == "owner": if users[username]["role"] == "owner":
raise HTTPException(400, "Нельзя удалить владельца") owners_count = sum(1 for u in users.values() if u.get("role") == "owner")
if owners_count <= 1:
raise HTTPException(400, "Нельзя удалить последнего владельца")
del users[username] del users[username]
save_users(users) save_users(users)
@@ -836,12 +838,12 @@ async def get_servers(user: dict = Depends(get_current_user)):
servers = [] servers = []
try: try:
# Владелец и администратор видят все серверы # Владелец и администратор видят все серверы
can_view_all = user.get("role") in ["owner", "admin"] or user.get("permissions", {}).get("view_all_resources", False) is_admin_or_owner = user.get("role") in ["owner", "admin"]
for server_dir in SERVERS_DIR.iterdir(): for server_dir in SERVERS_DIR.iterdir():
if server_dir.is_dir(): if server_dir.is_dir():
# Проверка доступа: владелец/админ видят всё, остальные только свои # Проверка доступа: владелец/админ видят всё, остальные только свои
if not can_view_all and server_dir.name not in user.get("servers", []): if not is_admin_or_owner and server_dir.name not in user.get("servers", []):
continue continue
config = load_server_config(server_dir.name) config = load_server_config(server_dir.name)
@@ -857,9 +859,14 @@ async def get_servers(user: dict = Depends(get_current_user)):
servers.append({ servers.append({
"name": server_dir.name, "name": server_dir.name,
"displayName": config.get("displayName", server_dir.name), "displayName": config.get("displayName", server_dir.name),
"status": "running" if is_running else "stopped" "status": "running" if is_running else "stopped",
"owner": config.get("owner", "unknown")
}) })
print(f"Найдено серверов для {user['username']} ({user.get('role', 'user')}): {len(servers)}")
print(f"[DEBUG] User: {user['username']} (role: {user.get('role', 'user')})")
print(f"[DEBUG] Is admin/owner: {is_admin_or_owner}")
print(f"[DEBUG] Servers found: {len(servers)}")
except Exception as e: except Exception as e:
print(f"Ошибка загрузки серверов: {e}") print(f"Ошибка загрузки серверов: {e}")
return servers return servers
@@ -867,9 +874,57 @@ async def get_servers(user: dict = Depends(get_current_user)):
@app.post("/api/servers/create") @app.post("/api/servers/create")
async def create_server(data: dict, user: dict = Depends(get_current_user)): async def create_server(data: dict, user: dict = Depends(get_current_user)):
server_name = data.get("name", "").strip() server_name = data.get("name", "").strip()
daemon_id = data.get("daemonId", "local")
if not server_name or not server_name.replace("_", "").replace("-", "").isalnum(): if not server_name or not server_name.replace("_", "").replace("-", "").isalnum():
raise HTTPException(400, "Недопустимое имя сервера") raise HTTPException(400, "Недопустимое имя сервера")
# Если создаем на демоне
if daemon_id != "local":
# Загружаем демоны
from daemons import load_daemons
daemons = load_daemons()
if daemon_id not in daemons:
raise HTTPException(404, "Демон не найден")
daemon = daemons[daemon_id]
# Отправляем запрос на создание сервера на демоне
url = f"http://{daemon['address']}:{daemon['port']}/api/servers/create"
headers = {"Authorization": f"Bearer {daemon['key']}"}
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.post(url, json={
"name": server_name,
"displayName": data.get("displayName", server_name),
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
"owner": user["username"]
}, headers=headers)
if response.status_code != 200:
raise HTTPException(400, f"Ошибка создания сервера на демоне: {response.text}")
except httpx.RequestError as e:
raise HTTPException(400, f"Ошибка подключения к демону: {str(e)}")
# Сохраняем информацию о сервере локально
config = {
"name": server_name,
"displayName": data.get("displayName", server_name),
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
"owner": user["username"],
"daemonId": daemon_id,
"daemonName": daemon["name"]
}
# Создаем локальную запись о сервере
server_path = SERVERS_DIR / f"{daemon_id}_{server_name}"
server_path.mkdir(parents=True, exist_ok=True)
save_server_config(f"{daemon_id}_{server_name}", config)
else:
# Создаем локально
server_path = SERVERS_DIR / server_name server_path = SERVERS_DIR / server_name
if server_path.exists(): if server_path.exists():
raise HTTPException(400, "Сервер с таким именем уже существует") raise HTTPException(400, "Сервер с таким именем уже существует")
@@ -880,21 +935,23 @@ async def create_server(data: dict, user: dict = Depends(get_current_user)):
"name": server_name, "name": server_name,
"displayName": data.get("displayName", server_name), "displayName": data.get("displayName", server_name),
"startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"), "startCommand": data.get("startCommand", "java -Xmx2G -Xms1G -jar server.jar nogui"),
"owner": user["username"] # Сохраняем владельца "owner": user["username"],
"daemonId": "local"
} }
save_server_config(server_name, config) save_server_config(server_name, config)
# Если пользователь не админ, автоматически выдаем ему доступ # Если пользователь не админ/owner, автоматически выдаем ему доступ
if user["role"] != "admin": if user["role"] not in ["admin", "owner"]:
users = load_users() users = load_users()
if user["username"] in users: if user["username"] in users:
if "servers" not in users[user["username"]]: if "servers" not in users[user["username"]]:
users[user["username"]]["servers"] = [] users[user["username"]]["servers"] = []
if server_name not in users[user["username"]]["servers"]: server_key = f"{daemon_id}_{server_name}" if daemon_id != "local" else server_name
users[user["username"]]["servers"].append(server_name) if server_key not in users[user["username"]]["servers"]:
users[user["username"]]["servers"].append(server_key)
save_users(users) save_users(users)
return {"message": "Сервер создан", "name": server_name} return {"message": "Сервер создан", "name": server_name, "daemonId": daemon_id}
@app.get("/api/servers/{server_name}/config") @app.get("/api/servers/{server_name}/config")
async def get_server_config(server_name: str, user: dict = Depends(get_current_user)): async def get_server_config(server_name: str, user: dict = Depends(get_current_user)):
@@ -1672,11 +1729,8 @@ async def change_user_role(username: str, role_data: RoleChange, current_user: d
if role_data.role not in valid_roles: if role_data.role not in valid_roles:
raise HTTPException(status_code=400, detail=f"Неверная роль") raise HTTPException(status_code=400, detail=f"Неверная роль")
# Если назначается новый owner, текущий owner становится admin # Разрешаем несколько владельцев (убрано ограничение на одного)
if role_data.role == "owner": # Теперь можно назначить несколько пользователей с ролью owner
for user in users.values():
if user.get("role") == "owner":
user["role"] = "admin"
old_role = users[username].get("role", "user") old_role = users[username].get("role", "user")
users[username]["role"] = role_data.role users[username]["role"] = role_data.role
@@ -1733,8 +1787,11 @@ async def ban_user(username: str, ban_data: BanRequest, current_user: dict = Dep
if username == current_user.get("username"): if username == current_user.get("username"):
raise HTTPException(status_code=400, detail="Нельзя заблокировать самого себя") raise HTTPException(status_code=400, detail="Нельзя заблокировать самого себя")
# Проверяем, что не блокируем последнего владельца
if users[username].get("role") == "owner": if users[username].get("role") == "owner":
raise HTTPException(status_code=400, detail="Нельзя заблокировать владельца") owners_count = sum(1 for u in users.values() if u.get("role") == "owner")
if owners_count <= 1:
raise HTTPException(status_code=400, detail="Нельзя заблокировать последнего владельца. Должен остаться хотя бы один владелец.")
users[username]["role"] = "banned" users[username]["role"] = "banned"
users[username]["permissions"] = { users[username]["permissions"] = {
@@ -1786,8 +1843,11 @@ async def delete_user(username: str, current_user: dict = Depends(get_current_us
if username == current_user.get("username"): if username == current_user.get("username"):
raise HTTPException(status_code=400, detail="Нельзя удалить самого себя") raise HTTPException(status_code=400, detail="Нельзя удалить самого себя")
# Проверяем, что не удаляем последнего владельца
if users[username].get("role") == "owner": if users[username].get("role") == "owner":
raise HTTPException(status_code=400, detail="Нельзя удалить владельца") owners_count = sum(1 for u in users.values() if u.get("role") == "owner")
if owners_count <= 1:
raise HTTPException(status_code=400, detail="Нельзя удалить последнего владельца. Должен остаться хотя бы один владелец.")
del users[username] del users[username]
save_users_dict(users) save_users_dict(users)
@@ -1864,6 +1924,16 @@ async def update_user_permissions(username: str, perms: PermissionsUpdate, curre
return {"message": f"Права пользователя {username} обновлены", "permissions": perms.permissions} return {"message": f"Права пользователя {username} обновлены", "permissions": perms.permissions}
# ============================================
# API для управления демонами
# ============================================
from daemons import router as daemons_router
# Подключаем роутер демонов
app.include_router(daemons_router)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)

View File

@@ -1 +1,58 @@
{} {
"1": {
"id": "1",
"title": "ававп",
"description": "вап",
"author": "Root",
"status": "closed",
"created_at": "2026-01-16T09:30:34.640417",
"updated_at": "2026-01-16T09:35:07.708933",
"messages": [
{
"author": "Root",
"text": "вап",
"timestamp": "2026-01-16T09:30:34.640417"
},
{
"author": "Root",
"text": "ап",
"timestamp": "2026-01-16T09:30:38.095207"
},
{
"author": "Root",
"text": "вапвп",
"timestamp": "2026-01-16T09:30:40.438134"
},
{
"author": "Root",
"text": "вапвпвап",
"timestamp": "2026-01-16T09:30:42.195576"
},
{
"author": "Root",
"text": "вавап",
"timestamp": "2026-01-16T09:30:44.371194"
},
{
"author": "Root",
"text": "вапвап",
"timestamp": "2026-01-16T09:30:45.779656"
},
{
"author": "Root",
"text": "вавап",
"timestamp": "2026-01-16T09:30:47.736753"
},
{
"author": "system",
"text": "Статус изменён на: В работе",
"timestamp": "2026-01-16T09:35:01.602729"
},
{
"author": "system",
"text": "Статус изменён на: Закрыт",
"timestamp": "2026-01-16T09:35:07.708933"
}
]
}
}

View File

@@ -1,6 +1,6 @@
{ {
"Root": { "admin": {
"username": "Root", "username": "admin",
"password": "$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG", "password": "$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG",
"role": "owner", "role": "owner",
"servers": [], "servers": [],
@@ -18,57 +18,5 @@
"tickets": [], "tickets": [],
"files": [] "files": []
} }
},
"MihailPrud": {
"username": "MihailPrud",
"password": "$2b$12$GfbQN4scE.b.mtUHofWWE.Dn1tQpT1zwLAxeICv90sHP4zGv0dc2G",
"role": "user",
"servers": [
"test",
"nya"
],
"permissions": {
"manage_users": false,
"manage_roles": false,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": false,
"view_all_resources": false
},
"resource_access": {
"servers": [
"test",
"nya"
],
"tickets": [],
"files": []
}
},
"arkonsad": {
"username": "arkonsad",
"password": "$2b$12$z.AYkfa/MlTYFd9rLNfBmu9JHOFKUe8YdddnqCmRqAxc7vGQeo392",
"role": "user",
"servers": [
"123",
"sdfsdf"
],
"permissions": {
"manage_users": false,
"manage_roles": false,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": false,
"view_all_resources": false
},
"resource_access": {
"servers": [
"123",
"sdfsdf"
],
"tickets": [],
"files": []
}
} }
} }

11
daemon/.env Normal file
View File

@@ -0,0 +1,11 @@
# Daemon Configuration
DAEMON_ID=daemon-1
DAEMON_NAME=Main
DAEMON_PORT=24444
DAEMON_KEY=JLgYFjTlFOqdyT49vmCqlXrLAuVE6FjiCdqf3zsZfr4
# Panel Connection (optional, for WebSocket)
PANEL_URL=ws://0.0.0.0:8000
# Servers Directory
SERVERS_DIR=./servers

20
daemon/.env.example Normal file
View File

@@ -0,0 +1,20 @@
# MC Panel Daemon Configuration
# Уникальный ID демона
DAEMON_ID=daemon-1
# Название демона (отображается в панели)
DAEMON_NAME=Main Server
# Порт, на котором будет работать демон
DAEMON_PORT=24444
# Секретный ключ для аутентификации (должен совпадать с ключом в панели)
# Сгенерируйте случайный ключ: python -c "import secrets; print(secrets.token_urlsafe(32))"
DAEMON_KEY=your-secret-key-here
# URL основной панели (для WebSocket подключения, опционально)
PANEL_URL=http://your-panel-url:8000
# Директория для серверов
SERVERS_DIR=./servers

195
daemon/README.md Normal file
View File

@@ -0,0 +1,195 @@
# MC Panel Daemon
Удаленный демон для управления серверами Minecraft. Устанавливается на отдельные физические серверы и подключается к основной панели управления.
## Установка
### Windows
1. Установите Python 3.8 или выше
2. Скопируйте папку `daemon` на удаленный сервер
3. Откройте командную строку в папке daemon
4. Запустите установку зависимостей:
```
install.bat
```
### Linux
1. Установите Python 3.8 или выше
2. Скопируйте папку `daemon` на удаленный сервер
3. Установите зависимости:
```bash
pip install -r requirements.txt
```
## Настройка
1. Скопируйте `.env.example` в `.env`:
```
copy .env.example .env
```
2. Отредактируйте `.env` файл:
```env
DAEMON_ID=daemon-1
DAEMON_NAME=Main Server
DAEMON_PORT=24444
DAEMON_KEY=your-secret-key-here
SERVERS_DIR=./servers
```
- `DAEMON_ID` - уникальный ID демона
- `DAEMON_NAME` - отображаемое имя демона
- `DAEMON_PORT` - порт для API (по умолчанию 24444)
- `DAEMON_KEY` - секретный ключ для аутентификации
- `SERVERS_DIR` - директория для серверов
3. Создайте секретный ключ:
```python
import secrets
print(secrets.token_urlsafe(32))
```
## Запуск
### Windows
```
start.bat
```
### Linux
```bash
python main.py
```
### Как сервис (Linux)
Создайте файл `/etc/systemd/system/mcpanel-daemon.service`:
```ini
[Unit]
Description=MC Panel Daemon
After=network.target
[Service]
Type=simple
User=mcpanel
WorkingDirectory=/path/to/daemon
ExecStart=/usr/bin/python3 /path/to/daemon/main.py
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
```
Запустите сервис:
```bash
sudo systemctl enable mcpanel-daemon
sudo systemctl start mcpanel-daemon
sudo systemctl status mcpanel-daemon
```
## Подключение к панели
1. Откройте основную панель управления
2. Перейдите в раздел "Демоны"
3. Нажмите "Добавить демон"
4. Заполните данные:
- **Название**: Main Server
- **IP адрес**: IP адрес сервера с демоном
- **Порт**: 24444 (или ваш порт)
- **Ключ демона**: ваш DAEMON_KEY из .env
5. Нажмите "Добавить"
## API Endpoints
Демон предоставляет следующие API endpoints:
- `GET /` - Информация о демоне
- `GET /api/status` - Статус демона и системы
- `GET /api/servers` - Список серверов
- `POST /api/servers/{name}/start` - Запустить сервер
- `POST /api/servers/{name}/stop` - Остановить сервер
- `POST /api/servers/{name}/command` - Отправить команду
- `GET /api/servers/{name}/stats` - Статистика сервера
## Безопасность
1. **Используйте сильный ключ** - генерируйте случайный ключ длиной минимум 32 символа
2. **Настройте файрвол** - разрешите доступ к порту демона только с IP основной панели
3. **Используйте HTTPS** - в продакшене используйте reverse proxy (nginx) с SSL
4. **Регулярно обновляйте** - следите за обновлениями и устанавливайте их
## Пример конфигурации nginx (с SSL)
```nginx
server {
listen 443 ssl http2;
server_name daemon.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:24444;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Структура директорий
```
daemon/
├── main.py # Основной файл демона
├── requirements.txt # Зависимости Python
├── .env # Конфигурация (создайте из .env.example)
├── .env.example # Пример конфигурации
├── install.bat # Скрипт установки (Windows)
├── start.bat # Скрипт запуска (Windows)
├── README.md # Эта документация
└── servers/ # Директория с серверами
├── server1/
│ ├── config.json
│ └── ...
└── server2/
├── config.json
└── ...
```
## Формат config.json для сервера
```json
{
"name": "server1",
"displayName": "My Minecraft Server",
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui"
}
```
## Troubleshooting
### Демон не запускается
- Проверьте, что Python установлен: `python --version`
- Проверьте, что все зависимости установлены: `pip list`
- Проверьте логи в консоли
### Панель не может подключиться к демону
- Проверьте, что демон запущен
- Проверьте файрвол и порты
- Проверьте, что ключ в панели совпадает с DAEMON_KEY
- Проверьте IP адрес и порт
### Сервер не запускается
- Проверьте startCommand в config.json
- Проверьте права доступа к файлам
- Проверьте логи сервера
## Поддержка
Если у вас возникли проблемы, создайте тикет в системе поддержки панели.

9
daemon/install.bat Normal file
View File

@@ -0,0 +1,9 @@
@echo off
echo Installing MC Panel Daemon dependencies...
pip install -r requirements.txt
echo.
echo Installation complete!
echo.
echo Please configure .env file before starting the daemon
echo Copy .env.example to .env and edit it
pause

307
daemon/main.py Normal file
View File

@@ -0,0 +1,307 @@
"""
MC Panel Daemon - Удаленный демон для управления серверами
Устанавливается на отдельные серверы и подключается к основной панели
"""
import asyncio
import json
import os
import psutil
import subprocess
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional
import websockets
from fastapi import FastAPI, HTTPException, Header
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(title="MC Panel Daemon")
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Конфигурация
DAEMON_ID = os.getenv("DAEMON_ID", "daemon-1")
DAEMON_NAME = os.getenv("DAEMON_NAME", "Main Server")
DAEMON_PORT = int(os.getenv("DAEMON_PORT", "24444"))
DAEMON_KEY = os.getenv("DAEMON_KEY", "") # Ключ для аутентификации
PANEL_URL = os.getenv("PANEL_URL", "") # URL основной панели для WebSocket
SERVERS_DIR = Path(os.getenv("SERVERS_DIR", "./servers"))
# Создаем директорию для серверов
SERVERS_DIR.mkdir(exist_ok=True)
# Хранилище процессов серверов
server_processes: Dict[str, subprocess.Popen] = {}
def verify_key(authorization: str = Header(None)) -> bool:
"""Проверка API ключа"""
if not DAEMON_KEY:
return True # Если ключ не установлен, разрешаем доступ
if not authorization:
raise HTTPException(status_code=401, detail="Missing authorization header")
if authorization != f"Bearer {DAEMON_KEY}":
raise HTTPException(status_code=403, detail="Invalid daemon key")
return True
@app.get("/")
async def root():
"""Информация о демоне"""
return {
"daemon_id": DAEMON_ID,
"daemon_name": DAEMON_NAME,
"status": "online",
"version": "1.0.0"
}
@app.get("/api/status")
async def get_status(authorization: str = Header(None)):
"""Получить статус демона и системы"""
# Проверка ключа опциональна для статуса
if DAEMON_KEY and authorization:
if authorization != f"Bearer {DAEMON_KEY}":
raise HTTPException(status_code=403, detail="Invalid daemon key")
cpu_percent = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
return {
"daemon_id": DAEMON_ID,
"daemon_name": DAEMON_NAME,
"status": "online",
"system": {
"cpu_usage": cpu_percent,
"memory_total": memory.total,
"memory_used": memory.used,
"memory_percent": memory.percent,
"disk_total": disk.total,
"disk_used": disk.used,
"disk_percent": disk.percent
},
"servers": {
"total": len(list(SERVERS_DIR.iterdir())),
"running": len(server_processes)
},
"timestamp": datetime.now().isoformat()
}
@app.get("/api/servers")
async def list_servers(authorization: str = Header(None)):
"""Список всех серверов на этом демоне"""
verify_key(authorization)
servers = []
for server_dir in SERVERS_DIR.iterdir():
if server_dir.is_dir():
config_file = server_dir / "config.json"
if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
servers.append({
"name": server_dir.name,
"display_name": config.get("displayName", server_dir.name),
"status": "running" if server_dir.name in server_processes else "stopped",
"daemon_id": DAEMON_ID
})
return servers
@app.post("/api/servers/{server_name}/start")
async def start_server(server_name: str, authorization: str = Header(None)):
"""Запустить сервер"""
verify_key(authorization)
server_dir = SERVERS_DIR / server_name
if not server_dir.exists():
raise HTTPException(status_code=404, detail="Server not found")
if server_name in server_processes:
raise HTTPException(status_code=400, detail="Server already running")
config_file = server_dir / "config.json"
if not config_file.exists():
raise HTTPException(status_code=400, detail="Server config not found")
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
start_command = config.get("startCommand", "")
if not start_command:
raise HTTPException(status_code=400, detail="Start command not configured")
try:
# Запускаем процесс
process = subprocess.Popen(
start_command,
shell=True,
cwd=str(server_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE
)
server_processes[server_name] = process
return {
"success": True,
"message": f"Server {server_name} started",
"pid": process.pid
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to start server: {str(e)}")
@app.post("/api/servers/{server_name}/stop")
async def stop_server(server_name: str, authorization: str = Header(None)):
"""Остановить сервер"""
verify_key(authorization)
if server_name not in server_processes:
raise HTTPException(status_code=400, detail="Server not running")
try:
process = server_processes[server_name]
process.terminate()
# Ждем завершения процесса
try:
process.wait(timeout=10)
except subprocess.TimeoutExpired:
process.kill()
del server_processes[server_name]
return {
"success": True,
"message": f"Server {server_name} stopped"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to stop server: {str(e)}")
@app.post("/api/servers/{server_name}/command")
async def send_command(server_name: str, command: dict, authorization: str = Header(None)):
"""Отправить команду в консоль сервера"""
verify_key(authorization)
if server_name not in server_processes:
raise HTTPException(status_code=400, detail="Server not running")
try:
process = server_processes[server_name]
cmd = command.get("command", "")
if process.stdin:
process.stdin.write(f"{cmd}\n".encode())
process.stdin.flush()
return {
"success": True,
"message": "Command sent"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to send command: {str(e)}")
@app.get("/api/servers/{server_name}/stats")
async def get_server_stats(server_name: str, authorization: str = Header(None)):
"""Получить статистику сервера"""
verify_key(authorization)
server_dir = SERVERS_DIR / server_name
if not server_dir.exists():
raise HTTPException(status_code=404, detail="Server not found")
is_running = server_name in server_processes
cpu = 0
memory = 0
if is_running:
try:
process = server_processes[server_name]
p = psutil.Process(process.pid)
cpu = p.cpu_percent(interval=0.1)
memory = p.memory_info().rss / 1024 / 1024 # MB
except:
pass
# Размер директории
disk_usage = sum(f.stat().st_size for f in server_dir.rglob('*') if f.is_file()) / 1024 / 1024 # MB
return {
"status": "running" if is_running else "stopped",
"cpu": cpu,
"memory": memory,
"disk": disk_usage
}
@app.post("/api/servers/create")
async def create_server_on_daemon(data: dict, authorization: str = Header(None)):
"""Создать сервер на этом демоне"""
verify_key(authorization)
server_name = data.get("name", "").strip()
if not server_name:
raise HTTPException(status_code=400, detail="Server name is required")
server_path = SERVERS_DIR / server_name
if server_path.exists():
raise HTTPException(status_code=400, detail="Server already exists")
try:
server_path.mkdir(parents=True)
# Сохраняем конфигурацию
config = {
"name": server_name,
"displayName": data.get("displayName", server_name),
"startCommand": data.get("startCommand", "java -Xmx2G -jar server.jar nogui"),
"owner": data.get("owner", "unknown")
}
config_file = server_path / "config.json"
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=2, ensure_ascii=False)
return {"message": "Server created successfully", "name": server_name}
except Exception as e:
# Если что-то пошло не так, удаляем папку
if server_path.exists():
import shutil
shutil.rmtree(server_path)
raise HTTPException(status_code=500, detail=f"Failed to create server: {str(e)}")
if __name__ == "__main__":
print(f"Starting MC Panel Daemon: {DAEMON_NAME} ({DAEMON_ID})")
print(f"Port: {DAEMON_PORT}")
print(f"Servers directory: {SERVERS_DIR}")
uvicorn.run(
app,
host="0.0.0.0",
port=DAEMON_PORT,
log_level="info"
)

5
daemon/requirements.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
websockets==12.0
psutil==5.9.6
python-dotenv==1.0.0

4
daemon/start.bat Normal file
View File

@@ -0,0 +1,4 @@
@echo off
echo Starting MC Panel Daemon...
python main.py
pause

61
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,61 @@
version: '3.8'
services:
# Backend для разработки
backend-dev:
build:
context: ./backend
dockerfile: Dockerfile
target: production
container_name: mc-panel-backend-dev
restart: unless-stopped
ports:
- "8000:8000"
environment:
- PORT=8000
- WORKERS=1
- PYTHONPATH=/app
- DEBUG=true
- LOG_LEVEL=DEBUG
volumes:
# Монтируем исходный код для hot reload
- ./backend:/app
- mc_servers_dev:/app/servers
- mc_data_dev:/app/data
- mc_logs_dev:/app/logs
networks:
- mc-panel-dev
command: ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# Frontend для разработки
frontend-dev:
build:
context: ./frontend
dockerfile: Dockerfile
target: development
container_name: mc-panel-frontend-dev
restart: unless-stopped
ports:
- "5173:5173"
volumes:
# Монтируем исходный код для hot reload
- ./frontend:/app
- /app/node_modules
networks:
- mc-panel-dev
environment:
- VITE_API_URL=http://localhost:8000
depends_on:
- backend-dev
volumes:
mc_servers_dev:
driver: local
mc_data_dev:
driver: local
mc_logs_dev:
driver: local
networks:
mc-panel-dev:
driver: bridge

View File

@@ -0,0 +1,65 @@
version: '3.8'
services:
# Backend сервис (локальная сборка)
backend:
build:
context: ./backend
dockerfile: Dockerfile
target: production
container_name: mc-panel-backend-local
restart: unless-stopped
ports:
- "8000:8000"
environment:
- PORT=8000
- WORKERS=2
- PYTHONPATH=/app
- DEBUG=false
env_file:
- ./backend/.env
volumes:
- mc_servers_local:/app/servers
- mc_data_local:/app/data
- mc_logs_local:/app/logs
networks:
- mc-panel-local
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
# Frontend сервис (локальная сборка)
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
target: production
container_name: mc-panel-frontend-local
restart: unless-stopped
ports:
- "80:80"
depends_on:
- backend
networks:
- mc-panel-local
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
mc_servers_local:
driver: local
mc_data_local:
driver: local
mc_logs_local:
driver: local
networks:
mc-panel-local:
driver: bridge

View File

@@ -1,65 +1,59 @@
version: '3.8' version: '3.8'
services: services:
# MC Panel приложение # Backend сервис
mc-panel: backend:
build: image: registry.nevetime.ru/mc-panel-backend:${IMAGE_TAG:-latest}
context: . container_name: mc-panel-backend
dockerfile: Dockerfile
container_name: mc-panel
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8000:8000" - "8000:8000"
environment: environment:
# ZITADEL OpenID Connect - PORT=8000
- ZITADEL_ISSUER=${ZITADEL_ISSUER} - WORKERS=2
- ZITADEL_CLIENT_ID=${ZITADEL_CLIENT_ID} - PYTHONPATH=/app
- ZITADEL_CLIENT_SECRET=${ZITADEL_CLIENT_SECRET} - DEBUG=false
env_file:
# URLs - ./backend/.env
- BASE_URL=${BASE_URL:-http://localhost:8000}
- FRONTEND_URL=${FRONTEND_URL:-http://localhost:3000}
# Security
- SECRET_KEY=${SECRET_KEY:-change-this-in-production}
# Python
- PYTHONUNBUFFERED=1
volumes: volumes:
# Персистентное хранилище для серверов - mc_servers:/app/servers
- ./data/servers:/app/backend/servers - mc_data:/app/data
# Персистентное хранилище для пользователей и тикетов - mc_logs:/app/logs
- ./data/users.json:/app/backend/users.json
- ./data/tickets.json:/app/backend/tickets.json
networks: networks:
- mc-panel-network - mc-panel-network
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/api/auth/oidc/providers"] test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 60s
# Nginx reverse proxy (опционально) # Frontend сервис
nginx: frontend:
image: nginx:alpine image: registry.nevetime.ru/mc-panel-frontend:${IMAGE_TAG:-latest}
container_name: mc-panel-nginx container_name: mc-panel-frontend
restart: unless-stopped restart: unless-stopped
ports: ports:
- "80:80" - "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on: depends_on:
- mc-panel - backend
networks: networks:
- mc-panel-network - mc-panel-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
mc_servers:
driver: local
mc_data:
driver: local
mc_logs:
driver: local
networks: networks:
mc-panel-network: mc-panel-network:
driver: bridge driver: bridge
volumes:
servers-data:
users-data:

207
frontend/Dockerfile Normal file
View File

@@ -0,0 +1,207 @@
# ================================
# MC Panel Frontend - Multi-Stage Dockerfile
# ================================
# Stage 1: Build Stage
FROM node:20-alpine AS builder
# Метаданные
LABEL maintainer="MC Panel Team" \
version="2.0.0" \
description="MC Panel Frontend - React Build Stage" \
component="frontend"
# Устанавливаем зависимости для сборки
RUN apk add --no-cache git python3 make g++
# Создаем рабочую директорию
WORKDIR /app
# Копируем package files для кеширования зависимостей
COPY package*.json ./
# Устанавливаем зависимости
RUN npm ci --silent
# Копируем исходный код
COPY . ./
# Собираем приложение для production
RUN npm run build
# Проверяем размер сборки
RUN du -sh dist/ && \
echo "Build completed successfully"
# ================================
# Stage 2: Production Stage (Nginx)
# ================================
FROM nginx:alpine AS production
# Метаданные
LABEL maintainer="MC Panel Team" \
version="2.0.0" \
description="MC Panel Frontend - Nginx Production Server" \
component="frontend"
# Устанавливаем дополнительные пакеты
RUN apk add --no-cache curl tini
# Создаем пользователя nginx если его нет
RUN addgroup -g 1000 -S mcpanel && \
adduser -u 1000 -D -S -G mcpanel mcpanel
# Копируем собранное приложение из builder stage
COPY --from=builder /app/dist /usr/share/nginx/html
# Создаем кастомную конфигурацию Nginx
RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
server {
listen 80;
listen [::]:80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Handle React Router (SPA)
location / {
try_files $uri $uri/ /index.html;
}
# API proxy (если нужно)
location /api/ {
proxy_pass http://backend:8000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket proxy (если нужно)
location /ws/ {
proxy_pass http://backend:8000/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check endpoint
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
}
EOF
# Создаем кастомную конфигурацию nginx.conf
RUN cat > /etc/nginx/nginx.conf << 'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
client_max_body_size 100M;
include /etc/nginx/conf.d/*.conf;
}
EOF
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost/health || exit 1
# Expose порт
EXPOSE 80
# Используем tini как init процесс
ENTRYPOINT ["/sbin/tini", "--"]
# Команда запуска
CMD ["nginx", "-g", "daemon off;"]
# ================================
# Stage 3: Development Stage
# ================================
FROM node:20-alpine AS development
# Метаданные
LABEL maintainer="MC Panel Team" \
version="2.0.0" \
description="MC Panel Frontend - Development Server" \
component="frontend"
# Устанавливаем зависимости для разработки
RUN apk add --no-cache git python3 make g++
# Создаем пользователя для разработки
RUN addgroup -g 1000 -S mcpanel && \
adduser -u 1000 -D -S -G mcpanel mcpanel
# Создаем рабочую директорию
WORKDIR /app
# Меняем владельца директории
RUN chown mcpanel:mcpanel /app
# Переключаемся на пользователя
USER mcpanel
# Копируем package files
COPY --chown=mcpanel:mcpanel package*.json ./
# Устанавливаем зависимости
RUN npm ci
# Копируем исходный код
COPY --chown=mcpanel:mcpanel . ./
# Expose порт для dev сервера
EXPOSE 5173
# Команда для разработки
CMD ["npm", "run", "dev"]

View File

@@ -1,5 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Server, Play, Square, Terminal, FolderOpen, HardDrive, Settings, Plus, Users as UsersIcon, LogOut, Menu, X, MessageSquare, UserCircle, Shield } from 'lucide-react'; import {
Server, Play, Square, Terminal, FolderOpen, Settings, Plus,
Users as UsersIcon, LogOut, Menu, X, MessageSquare, UserCircle,
Shield, Activity, HardDrive, Cpu, BarChart3, Home
} from 'lucide-react';
import Console from './components/Console'; import Console from './components/Console';
import FileManager from './components/FileManager'; import FileManager from './components/FileManager';
import Stats from './components/Stats'; import Stats from './components/Stats';
@@ -9,13 +13,12 @@ import Users from './components/Users';
import UserManagement from './components/UserManagement'; import UserManagement from './components/UserManagement';
import Tickets from './components/Tickets'; import Tickets from './components/Tickets';
import Profile from './components/Profile'; import Profile from './components/Profile';
import Daemons from './components/Daemons';
import Auth from './components/Auth'; import Auth from './components/Auth';
import ErrorBoundary from './components/ErrorBoundary'; import ErrorBoundary from './components/ErrorBoundary';
import ThemeSelector from './components/ThemeSelector';
import NotificationSystem, { notify } from './components/NotificationSystem'; import NotificationSystem, { notify } from './components/NotificationSystem';
import axios from 'axios'; import axios from 'axios';
import { API_URL } from './config'; import { API_URL } from './config';
import { getTheme } from './themes';
function App() { function App() {
const [token, setToken] = useState(localStorage.getItem('token')); const [token, setToken] = useState(localStorage.getItem('token'));
@@ -24,30 +27,23 @@ function App() {
const [selectedServer, setSelectedServer] = useState(null); const [selectedServer, setSelectedServer] = useState(null);
const [activeTab, setActiveTab] = useState('console'); const [activeTab, setActiveTab] = useState('console');
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
const [showUsers, setShowUsers] = useState(false);
const [showUserManagement, setShowUserManagement] = useState(false); const [showUserManagement, setShowUserManagement] = useState(false);
const [showDaemons, setShowDaemons] = useState(false);
const [showTickets, setShowTickets] = useState(false); const [showTickets, setShowTickets] = useState(false);
const [showProfile, setShowProfile] = useState(false); const [showProfile, setShowProfile] = useState(false);
const [viewingUsername, setViewingUsername] = useState(null);
const [connectionError, setConnectionError] = useState(false); const [connectionError, setConnectionError] = useState(false);
const [theme, setTheme] = useState(localStorage.getItem('theme') || 'modern');
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [currentView, setCurrentView] = useState('dashboard');
const currentTheme = getTheme(theme);
useEffect(() => { useEffect(() => {
// Проверяем callback от OpenID Connect
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
const callbackToken = urlParams.get('token'); const callbackToken = urlParams.get('token');
const callbackUsername = urlParams.get('username'); const callbackUsername = urlParams.get('username');
if (callbackToken && callbackUsername) { if (callbackToken && callbackUsername) {
// Сохраняем токен и обновляем состояние
localStorage.setItem('token', callbackToken); localStorage.setItem('token', callbackToken);
setToken(callbackToken); setToken(callbackToken);
setUser({ username: callbackUsername }); setUser({ username: callbackUsername });
// Очищаем URL
window.history.replaceState({}, document.title, window.location.pathname); window.history.replaceState({}, document.title, window.location.pathname);
return; return;
} }
@@ -99,6 +95,7 @@ function App() {
localStorage.setItem('token', data.access_token); localStorage.setItem('token', data.access_token);
setToken(data.access_token); setToken(data.access_token);
setUser({ username: data.username, role: data.role }); setUser({ username: data.username, role: data.role });
notify.success(`Добро пожаловать, ${data.username}!`);
}; };
const handleLogout = () => { const handleLogout = () => {
@@ -107,530 +104,483 @@ function App() {
setUser(null); setUser(null);
setServers([]); setServers([]);
setSelectedServer(null); setSelectedServer(null);
notify.info('Вы вышли из системы');
}; };
const handleServerDeleted = () => { const handleServerAction = async (serverName, action) => {
setSelectedServer(null);
loadServers();
};
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
const getRoleName = (role) => {
switch (role) {
case 'admin':
return 'Админ';
case 'support':
return 'Поддержка';
case 'banned':
return 'Забанен';
default:
return 'Пользователь';
}
};
const handleUsernameChange = (newToken, newUsername) => {
setToken(newToken);
setUser({ ...user, username: newUsername });
loadServers();
};
const handleViewProfile = (username) => {
setViewingUsername(username);
setShowProfile(true);
setShowUsers(false);
};
const startServer = async (serverName) => {
try { try {
const response = await axios.post( await axios.post(
`${API_URL}/api/servers/${serverName}/start`, `${API_URL}/api/servers/${serverName}/${action}`,
{}, {},
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
console.log('Сервер запущен:', response.data); notify.success(`Сервер ${action === 'start' ? 'запущен' : 'остановлен'}`);
notify('success', 'Сервер запущен', `Сервер "${serverName}" успешно запущен`);
setTimeout(() => {
loadServers(); loadServers();
}, 1000);
} catch (error) { } catch (error) {
console.error('Ошибка запуска сервера:', error); notify.error(`Ошибка: ${error.response?.data?.detail || error.message}`);
notify('error', 'Ошибка запуска', error.response?.data?.detail || 'Не удалось запустить сервер');
alert(error.response?.data?.detail || 'Ошибка запуска сервера');
}
};
const stopServer = async (serverName) => {
try {
const response = await axios.post(
`${API_URL}/api/servers/${serverName}/stop`,
{},
{ headers: { Authorization: `Bearer ${token}` } }
);
console.log('Сервер остановлен:', response.data);
notify('info', 'Сервер остановлен', `Сервер "${serverName}" успешно остановлен`);
setTimeout(() => {
loadServers();
}, 1000);
} catch (error) {
console.error('Ошибка остановки сервера:', error);
notify('error', 'Ошибка остановки', error.response?.data?.detail || 'Не удалось остановить сервер');
alert(error.response?.data?.detail || 'Ошибка остановки сервера');
} }
}; };
if (!token) { if (!token) {
return <Auth onLogin={handleLogin} />;
}
if (showUsers) {
return ( return (
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}> <ErrorBoundary>
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}> <Auth onLogin={handleLogin} />
<div className="px-6 py-4"> <NotificationSystem />
<div className="flex items-center justify-between"> </ErrorBoundary>
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
<Server className="w-6 h-6 text-white" />
</div>
<div>
<h1 className="text-xl font-bold">MC Panel</h1>
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
<span className={`text-sm ${currentTheme.textSecondary}`}>
{user?.username}
</span>
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
{getRoleName(user?.role)}
</span>
</div>
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
<button
onClick={() => setShowUsers(false)}
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
>
<Server className="w-4 h-4" />
Серверы
</button>
<button
onClick={handleLogout}
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<LogOut className="w-4 h-4" />
Выход
</button>
</div>
</div>
</div>
</header>
<Users token={token} theme={currentTheme} onViewProfile={handleViewProfile} />
</div>
);
}
if (showTickets) {
return (
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
<Server className="w-6 h-6 text-white" />
</div>
<div>
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>MC Panel</h1>
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
<span className={`text-sm ${currentTheme.textSecondary}`}>
{user?.username}
</span>
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
{getRoleName(user?.role)}
</span>
</div>
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
<button
onClick={() => setShowTickets(false)}
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
>
<Server className="w-4 h-4" />
Серверы
</button>
<button
onClick={handleLogout}
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<LogOut className="w-4 h-4" />
Выход
</button>
</div>
</div>
</div>
</header>
<Tickets token={token} user={user} theme={currentTheme} />
</div>
);
}
if (showProfile) {
return (
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}>
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}>
<div className="px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-3">
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
<Server className="w-6 h-6 text-white" />
</div>
<div>
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>MC Panel</h1>
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p>
</div>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
<span className={`text-sm ${currentTheme.textSecondary}`}>
{user?.username}
</span>
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
{getRoleName(user?.role)}
</span>
</div>
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
<button
onClick={() => {
setShowProfile(false);
setViewingUsername(null);
}}
className={`${currentTheme.card} ${currentTheme.hover} px-4 py-2 rounded-lg transition flex items-center gap-2`}
>
<Server className="w-4 h-4" />
Серверы
</button>
<button
onClick={handleLogout}
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<LogOut className="w-4 h-4" />
Выход
</button>
</div>
</div>
</div>
</header>
<Profile token={token} user={user} theme={currentTheme} onUsernameChange={handleUsernameChange} viewingUsername={viewingUsername} />
</div>
); );
} }
return ( return (
<div className={`min-h-screen ${currentTheme.primary} ${currentTheme.text} transition-colors duration-300`}> <ErrorBoundary>
<NotificationSystem theme={currentTheme} /> <div className="flex h-screen bg-dark-900 overflow-hidden">
{/* Header */} {/* Sidebar */}
<header className={`${currentTheme.secondary} ${currentTheme.border} border-b backdrop-blur-sm bg-opacity-95 sticky top-0 z-40`}> <aside className={`${sidebarOpen ? 'w-64' : 'w-20'} bg-dark-850 border-r border-dark-700 transition-all duration-300 flex flex-col`}>
<div className="px-6 py-4"> {/* Logo */}
<div className="flex items-center justify-between"> <div className="h-16 flex items-center justify-between px-4 border-b border-dark-700">
<div className="flex items-center gap-4"> {sidebarOpen && (
<div className="flex items-center gap-2">
<Server className="w-8 h-8 text-primary-500" />
<span className="text-xl font-bold bg-gradient-to-r from-primary-400 to-blue-500 bg-clip-text text-transparent">
MC Panel
</span>
</div>
)}
<button <button
onClick={() => setSidebarOpen(!sidebarOpen)} onClick={() => setSidebarOpen(!sidebarOpen)}
className={`lg:hidden ${currentTheme.hover} p-2 rounded-lg transition`} className="btn-icon"
> >
{sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />} {sidebarOpen ? <X className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button> </button>
<div className="flex items-center gap-3">
<div className={`${currentTheme.accent} p-2 rounded-lg`}>
<Server className="w-6 h-6 text-white" />
</div> </div>
<div>
<h1 className={`text-xl font-bold bg-gradient-to-r ${currentTheme.gradient} bg-clip-text text-transparent`}>MC Panel</h1> {/* Navigation */}
<p className={`text-xs ${currentTheme.textSecondary}`}>Управление серверами</p> <nav className="flex-1 p-4 space-y-2 overflow-y-auto">
</div>
</div>
</div>
<div className="flex items-center gap-3">
{connectionError && (
<div className="bg-red-500 bg-opacity-20 border border-red-500 px-3 py-1.5 rounded-lg text-sm text-red-400">
Нет связи
</div>
)}
<div className={`px-3 py-1.5 rounded-lg ${currentTheme.card} ${currentTheme.border} border`}>
<span className={`text-sm ${currentTheme.textSecondary}`}>
{user?.username}
</span>
<span className={`ml-2 text-xs px-2 py-0.5 rounded ${currentTheme.accent} text-white`}>
{getRoleName(user?.role)}
</span>
</div>
<ThemeSelector currentTheme={theme} onThemeChange={handleThemeChange} />
<button <button
onClick={() => { onClick={() => {
setShowProfile(true); setCurrentView('dashboard');
setViewingUsername(null); setSelectedServer(null);
}} }}
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`} className={currentView === 'dashboard' ? 'sidebar-item-active w-full' : 'sidebar-item w-full'}
> >
<UserCircle className="w-4 h-4" /> <Home className="w-5 h-5 flex-shrink-0" />
<span className="hidden sm:inline">Личный кабинет</span> {sidebarOpen && <span>Главная</span>}
</button> </button>
<button
onClick={() => setShowTickets(true)}
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<MessageSquare className="w-4 h-4" />
<span className="hidden sm:inline">Тикеты</span>
</button>
{user?.role === 'owner' && (
<button
onClick={() => setShowUserManagement(true)}
className="bg-yellow-500 hover:bg-yellow-600 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white"
title="Управление пользователями (только для владельца)"
>
<Shield className="w-4 h-4" />
<span className="hidden sm:inline">Управление</span>
</button>
)}
{user?.role === 'admin' && (
<button
onClick={() => setShowUsers(true)}
className={`${currentTheme.accent} ${currentTheme.accentHover} px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<UsersIcon className="w-4 h-4" />
<span className="hidden sm:inline">Пользователи</span>
</button>
)}
<button
onClick={handleLogout}
className={`${currentTheme.danger} hover:opacity-90 px-4 py-2 rounded-lg transition flex items-center gap-2 text-white`}
>
<LogOut className="w-4 h-4" />
<span className="hidden sm:inline">Выход</span>
</button>
</div>
</div>
</div>
</header>
<div className="flex h-[calc(100vh-89px)]"> {sidebarOpen && (
{/* Sidebar */} <div className="pt-4 pb-2">
<aside className={`${sidebarOpen ? 'w-80' : 'w-0'} ${currentTheme.secondary} ${currentTheme.border} border-r transition-all duration-300 overflow-hidden`}> <div className="flex items-center justify-between px-4 mb-2">
<div className="p-4 h-full flex flex-col"> <span className="text-xs font-semibold text-gray-500 uppercase">Серверы</span>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">Мои серверы</h2>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className={`${currentTheme.accent} ${currentTheme.accentHover} p-2 rounded-lg transition text-white`} className="p-1 rounded hover:bg-dark-700 text-primary-500"
title="Создать сервер"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
</button> </button>
</div> </div>
</div>
)}
<div className="flex-1 overflow-y-auto space-y-2">
{servers.map((server) => ( {servers.map((server) => (
<div <button
key={server.name} key={server.name}
className={`p-4 rounded-lg cursor-pointer transition-all duration-200 ${ onClick={() => {
selectedServer === server.name setSelectedServer(server);
? `${currentTheme.accent} text-white shadow-lg` setCurrentView('server');
: `${currentTheme.card} ${currentTheme.hover}` setActiveTab('console');
}`}
onClick={() => setSelectedServer(server.name)}
>
<div className="flex items-center justify-between mb-3">
<span className="font-medium truncate">{server.displayName}</span>
<div className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${
server.status === 'running' ? 'bg-green-400 animate-pulse' : 'bg-gray-500'
}`}
/>
</div>
</div>
<div className="flex gap-2">
{server.status === 'stopped' ? (
<button
onClick={(e) => {
e.stopPropagation();
startServer(server.name);
}} }}
className="flex-1 bg-green-600 hover:bg-green-700 py-1.5 rounded-lg text-sm flex items-center justify-center gap-1.5 text-white transition" className={selectedServer?.name === server.name ? 'sidebar-item-active w-full' : 'sidebar-item w-full'}
> >
<Play className="w-3.5 h-3.5" /> <div className={`w-2 h-2 rounded-full flex-shrink-0 ${
Запустить server.status === 'running' ? 'bg-green-500 shadow-glow' : 'bg-gray-600'
</button> }`} />
) : ( {sidebarOpen && (
<button <div className="flex-1 text-left truncate">
onClick={(e) => { <div className="text-sm font-medium">{server.displayName}</div>
e.stopPropagation(); <div className="text-xs text-gray-500">{server.status === 'running' ? 'Запущен' : 'Остановлен'}</div>
stopServer(server.name); </div>
}}
className="flex-1 bg-red-600 hover:bg-red-700 py-1.5 rounded-lg text-sm flex items-center justify-center gap-1.5 text-white transition"
>
<Square className="w-3.5 h-3.5" />
Остановить
</button>
)} )}
</div> </button>
</div>
))} ))}
{servers.length === 0 && (
<div className={`text-center py-12 ${currentTheme.textSecondary}`}> {sidebarOpen && servers.length === 0 && (
<Server className="w-12 h-12 mx-auto mb-3 opacity-50" /> <div className="text-center py-8 text-gray-500 text-sm">
<p className="text-sm">Нет серверов</p> <Server className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p className="text-xs mt-1">Создайте первый сервер</p> <p>Нет серверов</p>
<button
onClick={() => setShowCreateModal(true)}
className="btn-primary mt-2 text-xs"
>
Создать сервер
</button>
</div> </div>
)} )}
</nav>
{/* Bottom section */}
<div className="p-4 border-t border-dark-700 space-y-2">
{(user?.role === 'owner' || user?.role === 'admin') && (
<>
<button
onClick={() => {
setShowUserManagement(true);
setCurrentView('management');
}}
className="sidebar-item w-full"
>
<Shield className="w-5 h-5 flex-shrink-0 text-yellow-500" />
{sidebarOpen && <span>Управление</span>}
</button>
<button
onClick={() => {
setShowDaemons(true);
setCurrentView('daemons');
}}
className="sidebar-item w-full"
>
<Server className="w-5 h-5 flex-shrink-0 text-blue-500" />
{sidebarOpen && <span>Демоны</span>}
</button>
</>
)}
<button
onClick={() => {
setShowTickets(true);
setCurrentView('tickets');
}}
className="sidebar-item w-full"
>
<MessageSquare className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Тикеты</span>}
</button>
<button
onClick={() => {
setShowProfile(true);
setCurrentView('profile');
}}
className="sidebar-item w-full"
>
<UserCircle className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Профиль</span>}
</button>
<button
onClick={handleLogout}
className="sidebar-item w-full text-red-400 hover:text-red-300 hover:bg-red-600/10"
>
<LogOut className="w-5 h-5 flex-shrink-0" />
{sidebarOpen && <span>Выход</span>}
</button>
{sidebarOpen && user && (
<div className="pt-2 border-t border-dark-700">
<div className="text-xs text-gray-500">
<div className="font-medium text-gray-300">{user.username}</div>
<div className="capitalize">{user.role}</div>
</div> </div>
</div> </div>
)}
</div>
</aside> </aside>
{/* Main Content */} {/* Main content */}
<main className="flex-1 flex flex-col overflow-hidden"> <main className="flex-1 flex flex-col overflow-hidden">
{selectedServer ? ( {/* Header */}
<> <header className="h-16 bg-dark-850 border-b border-dark-700 flex items-center justify-between px-6">
{/* Server Header with Controls */} <div>
<div className={`${currentTheme.secondary} ${currentTheme.border} border-b px-6 py-3 flex items-center justify-between`}> <h1 className="text-2xl font-bold text-gray-100">
<div className="flex items-center gap-3"> {currentView === 'dashboard' && 'Панель управления'}
<Server className="w-5 h-5" /> {currentView === 'server' && selectedServer?.displayName}
<span className="font-semibold text-lg"> {currentView === 'management' && 'Управление пользователями'}
{servers.find(s => s.name === selectedServer)?.displayName || selectedServer} {currentView === 'daemons' && 'Управление демонами'}
</span> {currentView === 'tickets' && 'Тикеты поддержки'}
<span className={`px-2 py-1 rounded text-xs font-medium ${ {currentView === 'profile' && 'Профиль'}
servers.find(s => s.name === selectedServer)?.status === 'running' </h1>
? 'bg-green-600 text-white' {currentView === 'server' && selectedServer && (
: 'bg-gray-600 text-white' <p className="text-sm text-gray-500">
}`}> {selectedServer.name} {selectedServer.status === 'running' ? 'Запущен' : 'Остановлен'}
{servers.find(s => s.name === selectedServer)?.status === 'running' ? 'Запущен' : 'Остановлен'} </p>
</span> )}
</div> </div>
{currentView === 'server' && selectedServer && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{servers.find(s => s.name === selectedServer)?.status === 'stopped' ? ( {selectedServer.status === 'stopped' ? (
<button <button
onClick={() => startServer(selectedServer)} onClick={() => handleServerAction(selectedServer.name, 'start')}
className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 text-white transition shadow-lg" className="btn-success flex items-center gap-2"
> >
<Play className="w-4 h-4" /> <Play className="w-4 h-4" />
Запустить Запустить
</button> </button>
) : ( ) : (
<button <button
onClick={() => stopServer(selectedServer)} onClick={() => handleServerAction(selectedServer.name, 'stop')}
className="bg-gray-700 hover:bg-gray-600 px-4 py-2 rounded-lg text-sm font-medium flex items-center gap-2 text-white transition shadow-lg" className="btn-danger flex items-center gap-2"
> >
<Square className="w-4 h-4" /> <Square className="w-4 h-4" />
Сброс Остановить
</button> </button>
)} )}
</div> </div>
)}
</header>
{/* Content area */}
<div className="flex-1 overflow-auto p-6">
{connectionError && (
<div className="card p-4 mb-4 bg-red-600/10 border-red-600/30">
<p className="text-red-400"> Ошибка подключения к серверу</p>
</div>
)}
{currentView === 'dashboard' && (
<div className="space-y-6 animate-fade-in">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="stat-card">
<div className="stat-icon">
<Server className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold">{servers.length}</div>
<div className="text-sm text-gray-500">Всего серверов</div>
</div>
</div> </div>
{/* Tabs */} <div className="stat-card">
<div className={`${currentTheme.secondary} ${currentTheme.border} border-b flex overflow-x-auto`}> <div className="stat-icon bg-green-600/20 text-green-400">
{[ <Activity className="w-6 h-6" />
{ id: 'console', icon: Terminal, label: 'Консоль' }, </div>
{ id: 'files', icon: FolderOpen, label: 'Файлы' }, <div>
{ id: 'stats', icon: HardDrive, label: 'Статистика' }, <div className="text-2xl font-bold text-green-400">
{ id: 'settings', icon: Settings, label: 'Настройки' }, {servers.filter(s => s.status === 'running').length}
].map((tab) => ( </div>
<div className="text-sm text-gray-500">Запущено</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon bg-gray-600/20 text-gray-400">
<Square className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold text-gray-400">
{servers.filter(s => s.status === 'stopped').length}
</div>
<div className="text-sm text-gray-500">Остановлено</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon bg-purple-600/20 text-purple-400">
<UserCircle className="w-6 h-6" />
</div>
<div>
<div className="text-2xl font-bold text-purple-400">{user?.username}</div>
<div className="text-sm text-gray-500 capitalize">{user?.role}</div>
</div>
</div>
</div>
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Ваши серверы</h2>
<button <button
key={tab.id} onClick={() => setShowCreateModal(true)}
onClick={() => setActiveTab(tab.id)} className="btn-primary flex items-center gap-2"
className={`px-6 py-4 flex items-center gap-2 transition-all duration-200 border-b-2 ${
activeTab === tab.id
? `${currentTheme.accent.replace('bg-', 'border-')} ${currentTheme.text}`
: `border-transparent ${currentTheme.textSecondary} ${currentTheme.hover}`
}`}
> >
<tab.icon className="w-4 h-4" /> <Plus className="w-4 h-4" />
<span className="font-medium">{tab.label}</span> Создать сервер
</button> </button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{servers.map((server) => (
<div
key={server.name}
onClick={() => {
setSelectedServer(server);
setCurrentView('server');
}}
className="server-card"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className={`p-3 rounded-lg ${
server.status === 'running'
? 'bg-green-600/20 text-green-400'
: 'bg-gray-600/20 text-gray-400'
}`}>
<Server className="w-6 h-6" />
</div>
<div>
<h3 className="font-semibold text-lg">{server.displayName}</h3>
<p className="text-sm text-gray-500">{server.name}</p>
</div>
</div>
<span className={server.status === 'running' ? 'badge-success' : 'badge-danger'}>
{server.status === 'running' ? 'Запущен' : 'Остановлен'}
</span>
</div>
<div className="flex items-center gap-2">
{server.status === 'stopped' ? (
<button
onClick={(e) => {
e.stopPropagation();
handleServerAction(server.name, 'start');
}}
className="btn-success flex-1 flex items-center justify-center gap-2"
>
<Play className="w-4 h-4" />
Запустить
</button>
) : (
<button
onClick={(e) => {
e.stopPropagation();
handleServerAction(server.name, 'stop');
}}
className="btn-danger flex-1 flex items-center justify-center gap-2"
>
<Square className="w-4 h-4" />
Остановить
</button>
)}
<button
onClick={(e) => {
e.stopPropagation();
setSelectedServer(server);
setCurrentView('server');
}}
className="btn-secondary"
>
<Terminal className="w-4 h-4" />
</button>
</div>
</div>
))} ))}
</div> </div>
{/* Content */} {servers.length === 0 && (
<div className="flex-1 overflow-hidden"> <div className="card p-12 text-center">
<ErrorBoundary> <Server className="w-16 h-16 mx-auto mb-4 text-gray-600" />
{activeTab === 'console' && <Console serverName={selectedServer} token={token} theme={currentTheme} />} <h3 className="text-xl font-semibold mb-2">Нет серверов</h3>
{activeTab === 'files' && <FileManager serverName={selectedServer} token={token} theme={currentTheme} />} <p className="text-gray-500 mb-4">Создайте свой первый сервер</p>
{activeTab === 'stats' && <Stats serverName={selectedServer} token={token} theme={currentTheme} />} <button
{activeTab === 'settings' && ( onClick={() => setShowCreateModal(true)}
<ServerSettings className="btn-primary"
serverName={selectedServer} >
token={token} <Plus className="w-4 h-4 inline mr-2" />
user={user} Создать сервер
theme={currentTheme} </button>
onDeleted={handleServerDeleted} </div>
/>
)} )}
</ErrorBoundary>
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center">
<div className="text-center">
<div className={`${currentTheme.card} p-8 rounded-2xl ${currentTheme.border} border`}>
<Server className={`w-16 h-16 mx-auto mb-4 ${currentTheme.textSecondary} opacity-50`} />
<p className="text-xl font-medium mb-2">Выберите сервер</p>
<p className={`text-sm ${currentTheme.textSecondary}`}>
Выберите сервер из списка слева или создайте новый
</p>
</div>
</div> </div>
</div> </div>
)} )}
</main>
{currentView === 'server' && selectedServer && (
<div className="space-y-4 animate-fade-in">
{/* Tabs */}
<div className="card p-2 flex gap-2 overflow-x-auto">
<button
onClick={() => setActiveTab('console')}
className={activeTab === 'console' ? 'tab-active' : 'tab'}
>
<Terminal className="w-4 h-4 inline mr-2" />
Консоль
</button>
<button
onClick={() => setActiveTab('files')}
className={activeTab === 'files' ? 'tab-active' : 'tab'}
>
<FolderOpen className="w-4 h-4 inline mr-2" />
Файлы
</button>
<button
onClick={() => setActiveTab('stats')}
className={activeTab === 'stats' ? 'tab-active' : 'tab'}
>
<BarChart3 className="w-4 h-4 inline mr-2" />
Статистика
</button>
<button
onClick={() => setActiveTab('settings')}
className={activeTab === 'settings' ? 'tab-active' : 'tab'}
>
<Settings className="w-4 h-4 inline mr-2" />
Настройки
</button>
</div> </div>
{/* Tab content */}
<div className="card p-6">
{activeTab === 'console' && <Console serverName={selectedServer.name} token={token} />}
{activeTab === 'files' && <FileManager serverName={selectedServer.name} token={token} />}
{activeTab === 'stats' && <Stats serverName={selectedServer.name} token={token} />}
{activeTab === 'settings' && (
<ServerSettings
serverName={selectedServer.name}
token={token}
onUpdate={loadServers}
/>
)}
</div>
</div>
)}
</div>
</main>
{/* Modals */}
{showCreateModal && ( {showCreateModal && (
<CreateServerModal <CreateServerModal
token={token} token={token}
theme={currentTheme}
onClose={() => setShowCreateModal(false)} onClose={() => setShowCreateModal(false)}
onCreated={loadServers} onSuccess={() => {
setShowCreateModal(false);
loadServers();
}}
/> />
)} )}
{showUserManagement && ( {showUserManagement && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"> <div className="modal-overlay" onClick={() => setShowUserManagement(false)}>
<div className={`${currentTheme.card} rounded-2xl ${currentTheme.border} border w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col`}> <div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className={`${currentTheme.secondary} px-6 py-4 ${currentTheme.border} border-b flex items-center justify-between`}> <UserManagement token={token} currentUser={user} />
<h2 className="text-2xl font-bold flex items-center gap-2">
<Shield className="w-6 h-6 text-yellow-400" />
Управление пользователями
</h2>
<button
onClick={() => setShowUserManagement(false)}
className={`${currentTheme.hover} p-2 rounded-lg transition`}
>
<X className="w-5 h-5" />
</button>
</div>
<div className="flex-1 overflow-y-auto">
<UserManagement
currentUser={user}
addNotification={notify}
/>
</div>
</div> </div>
</div> </div>
)} )}
{showDaemons && (
<div className="modal-overlay" onClick={() => setShowDaemons(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<Daemons token={token} />
</div> </div>
</div>
)}
{showTickets && (
<div className="modal-overlay" onClick={() => setShowTickets(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<Tickets token={token} user={user} onClose={() => setShowTickets(false)} />
</div>
</div>
)}
{showProfile && (
<div className="modal-overlay" onClick={() => setShowProfile(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<Profile token={token} user={user} onClose={() => setShowProfile(false)} />
</div>
</div>
)}
<NotificationSystem />
</div>
</ErrorBoundary>
); );
} }

View File

@@ -3,7 +3,7 @@ import { Send } from 'lucide-react';
import axios from 'axios'; import axios from 'axios';
import { API_URL, WS_URL } from '../config'; import { API_URL, WS_URL } from '../config';
export default function Console({ serverName, token, theme }) { export default function Console({ serverName, token }) {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [command, setCommand] = useState(''); const [command, setCommand] = useState('');
const logsEndRef = useRef(null); const logsEndRef = useRef(null);
@@ -84,11 +84,11 @@ export default function Console({ serverName, token, theme }) {
}; };
return ( return (
<div className={`flex flex-col h-full ${theme.primary}`}> <div className="flex flex-col h-full">
{/* Консоль */} {/* Консоль */}
<div className={`flex-1 overflow-y-auto p-4 font-mono text-sm ${theme.console || theme.secondary}`}> <div className="console-terminal flex-1 overflow-y-auto min-h-[400px] max-h-[600px]">
{logs.length === 0 ? ( {logs.length === 0 ? (
<div className={theme.textSecondary}>Консоль пуста. Запустите сервер для просмотра логов.</div> <div className="text-gray-500">Консоль пуста. Запустите сервер для просмотра логов.</div>
) : ( ) : (
logs.map((log, index) => ( logs.map((log, index) => (
<div key={index} className="whitespace-pre-wrap leading-relaxed"> <div key={index} className="whitespace-pre-wrap leading-relaxed">
@@ -100,17 +100,17 @@ export default function Console({ serverName, token, theme }) {
</div> </div>
{/* Поле ввода команды */} {/* Поле ввода команды */}
<form onSubmit={sendCommand} className={`${theme.border} border-t p-4 flex gap-2`}> <form onSubmit={sendCommand} className="border-t border-dark-700 p-4 flex gap-2 bg-dark-850">
<input <input
type="text" type="text"
value={command} value={command}
onChange={(e) => setCommand(e.target.value)} onChange={(e) => setCommand(e.target.value)}
placeholder="Введите команду и нажмите Enter для отправки, используйте стрелки для навигации между предыдущими командами" placeholder="Введите команду..."
className={`flex-1 ${theme.input} ${theme.border} border rounded-lg px-4 py-2.5 ${theme.text} placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-green-500 transition`} className="input flex-1"
/> />
<button <button
type="submit" type="submit"
className={`${theme.success} ${theme.successHover} px-6 py-2.5 rounded-lg flex items-center gap-2 text-white font-medium transition shadow-lg`} className="btn-success flex items-center gap-2"
> >
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
Отправить Отправить

View File

@@ -1,16 +1,36 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { X } from 'lucide-react'; import { X, Server } from 'lucide-react';
import axios from 'axios'; import axios from 'axios';
import { API_URL } from '../config'; import { API_URL } from '../config';
import { notify } from './NotificationSystem'; import { notify } from './NotificationSystem';
export default function CreateServerModal({ token, theme, onClose, onCreated }) { export default function CreateServerModal({ token, onClose, onSuccess }) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
displayName: '', displayName: '',
startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui' startCommand: 'java -Xmx2G -Xms1G -jar server.jar nogui',
daemonId: 'local' // По умолчанию локальный
}); });
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [daemons, setDaemons] = useState([]);
const [loadingDaemons, setLoadingDaemons] = useState(true);
useEffect(() => {
loadDaemons();
}, []);
const loadDaemons = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/daemons`, {
headers: { Authorization: `Bearer ${token}` }
});
setDaemons(data.filter(d => d.status === 'online')); // Только онлайн демоны
} catch (error) {
console.error('Ошибка загрузки демонов:', error);
} finally {
setLoadingDaemons(false);
}
};
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@@ -23,11 +43,10 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
notify('success', 'Сервер создан', `Сервер "${formData.displayName}" успешно создан`); notify('success', 'Сервер создан', `Сервер "${formData.displayName}" успешно создан`);
onCreated(); if (onSuccess) onSuccess();
onClose(); onClose();
} catch (error) { } catch (error) {
notify('error', 'Ошибка создания', error.response?.data?.detail || 'Не удалось создать сервер'); notify('error', 'Ошибка создания', error.response?.data?.detail || 'Не удалось создать сервер');
alert(error.response?.data?.detail || 'Ошибка создания сервера');
} finally { } finally {
setLoading(false); setLoading(false);
} }
@@ -35,12 +54,12 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className={`${theme.secondary} rounded-2xl p-6 w-full max-w-md shadow-2xl ${theme.border} border`}> <div className="bg-dark-800 rounded-2xl p-6 w-full max-w-md shadow-2xl border border-gray-700">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className={`text-xl font-bold ${theme.text}`}>Создать сервер</h2> <h2 className="text-xl font-bold text-white">Создать сервер</h2>
<button <button
onClick={onClose} onClick={onClose}
className={`${theme.textSecondary} hover:${theme.text} transition`} className="text-gray-400 hover:text-white transition"
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
@@ -48,7 +67,34 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">
Демон
</label>
{loadingDaemons ? (
<div className="input text-gray-400">Загрузка демонов...</div>
) : (
<select
value={formData.daemonId}
onChange={(e) => setFormData({ ...formData, daemonId: e.target.value })}
className="w-full bg-dark-700 border border-gray-600 rounded-xl px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
>
<option value="local">Локальный (эта машина)</option>
{daemons.map((daemon) => (
<option key={daemon.id} value={daemon.id}>
{daemon.name} ({daemon.address}:{daemon.port})
</option>
))}
</select>
)}
<p className="text-xs text-gray-400 mt-1">
{formData.daemonId === 'local'
? 'Сервер будет создан на этой машине'
: 'Сервер будет создан на удаленном демоне'}
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">
Имя папки (только латиница, цифры, _ и -) Имя папки (только латиница, цифры, _ и -)
</label> </label>
<input <input
@@ -56,13 +102,13 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
required required
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input"
placeholder="my_server" placeholder="my_server"
/> />
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">
Отображаемое имя Отображаемое имя
</label> </label>
<input <input
@@ -70,13 +116,13 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
required required
value={formData.displayName} value={formData.displayName}
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })} onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input"
placeholder="Мой сервер" placeholder="Мой сервер"
/> />
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">
Команда запуска Команда запуска
</label> </label>
<input <input
@@ -84,7 +130,7 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
required required
value={formData.startCommand} value={formData.startCommand}
onChange={(e) => setFormData({ ...formData, startCommand: e.target.value })} onChange={(e) => setFormData({ ...formData, startCommand: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input"
/> />
</div> </div>
@@ -92,14 +138,14 @@ export default function CreateServerModal({ token, theme, onClose, onCreated })
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className={`flex-1 ${theme.card} ${theme.hover} px-4 py-2 rounded-xl transition`} className="flex-1 bg-dark-700 hover:bg-dark-600 px-4 py-2 rounded-xl transition text-white"
> >
Отмена Отмена
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className={`flex-1 ${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl disabled:opacity-50 transition text-white`} className="flex-1 btn-primary disabled:opacity-50"
> >
{loading ? 'Создание...' : 'Создать'} {loading ? 'Создание...' : 'Создать'}
</button> </button>

View File

@@ -3,7 +3,7 @@ import { X } from 'lucide-react';
import axios from 'axios'; import axios from 'axios';
import { API_URL } from '../config'; import { API_URL } from '../config';
export default function CreateTicketModal({ token, theme, onClose, onCreated }) { export default function CreateTicketModal({ token, onClose, onCreated }) {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
title: '', title: '',
description: '' description: ''
@@ -30,12 +30,12 @@ export default function CreateTicketModal({ token, theme, onClose, onCreated })
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className={`${theme.secondary} rounded-2xl p-6 w-full max-w-md shadow-2xl ${theme.border} border`}> <div className="bg-dark-800 rounded-2xl p-6 w-full max-w-md shadow-2xl border border-gray-700">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<h2 className={`text-xl font-bold ${theme.text}`}>Создать тикет</h2> <h2 className="text-xl font-bold text-white">Создать тикет</h2>
<button <button
onClick={onClose} onClick={onClose}
className={`${theme.textSecondary} hover:${theme.text} transition`} className="text-gray-400 hover:text-white transition"
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
@@ -43,7 +43,7 @@ export default function CreateTicketModal({ token, theme, onClose, onCreated })
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">
Тема тикета Тема тикета
</label> </label>
<input <input
@@ -51,20 +51,20 @@ export default function CreateTicketModal({ token, theme, onClose, onCreated })
required required
value={formData.title} value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })} onChange={(e) => setFormData({ ...formData, title: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input"
placeholder="Краткое описание проблемы" placeholder="Краткое описание проблемы"
/> />
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">
Описание Описание
</label> </label>
<textarea <textarea
required required
value={formData.description} value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })} onChange={(e) => setFormData({ ...formData, description: e.target.value })}
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition resize-none`} className="w-full bg-dark-800 border-gray-700 border rounded-xl px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition resize-none"
placeholder="Подробное описание проблемы" placeholder="Подробное описание проблемы"
rows={5} rows={5}
/> />
@@ -74,14 +74,14 @@ export default function CreateTicketModal({ token, theme, onClose, onCreated })
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className={`flex-1 ${theme.card} ${theme.hover} px-4 py-2 rounded-xl transition`} className="flex-1 bg-dark-700 hover:bg-dark-600 px-4 py-2 rounded-xl transition text-white"
> >
Отмена Отмена
</button> </button>
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className={`flex-1 ${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl disabled:opacity-50 transition text-white`} className="flex-1 btn-primary disabled:opacity-50"
> >
{loading ? 'Создание...' : 'Создать'} {loading ? 'Создание...' : 'Создать'}
</button> </button>

View File

@@ -0,0 +1,382 @@
import { useState, useEffect } from 'react';
import { Server, Plus, Trash2, Edit, RefreshCw, CheckCircle, XCircle, Activity } from 'lucide-react';
import axios from 'axios';
import { API_URL } from '../config';
import { notify } from './NotificationSystem';
export default function Daemons({ token }) {
const [daemons, setDaemons] = useState([]);
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [editingDaemon, setEditingDaemon] = useState(null);
const [formData, setFormData] = useState({
name: '',
address: '',
port: 24444,
key: '',
remarks: ''
});
useEffect(() => {
loadDaemons();
const interval = setInterval(loadDaemons, 10000); // Обновляем каждые 10 секунд
return () => clearInterval(interval);
}, []);
const loadDaemons = async () => {
try {
const { data } = await axios.get(`${API_URL}/api/daemons`, {
headers: { Authorization: `Bearer ${token}` }
});
setDaemons(data);
setLoading(false);
} catch (error) {
console.error('Ошибка загрузки демонов:', error);
setLoading(false);
}
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
if (editingDaemon) {
await axios.put(
`${API_URL}/api/daemons/${editingDaemon.id}`,
formData,
{ headers: { Authorization: `Bearer ${token}` } }
);
notify('success', 'Демон обновлен', 'Демон успешно обновлен');
} else {
await axios.post(
`${API_URL}/api/daemons`,
formData,
{ headers: { Authorization: `Bearer ${token}` } }
);
notify('success', 'Демон добавлен', 'Демон успешно добавлен');
}
setShowAddModal(false);
setEditingDaemon(null);
setFormData({ name: '', address: '', port: 24444, key: '', remarks: '' });
loadDaemons();
} catch (error) {
notify('error', 'Ошибка', error.response?.data?.detail || 'Не удалось сохранить демон');
}
};
const handleDelete = async (daemonId) => {
if (!confirm('Вы уверены, что хотите удалить этот демон?')) return;
try {
await axios.delete(`${API_URL}/api/daemons/${daemonId}`, {
headers: { Authorization: `Bearer ${token}` }
});
notify('success', 'Демон удален', 'Демон успешно удален');
loadDaemons();
} catch (error) {
notify('error', 'Ошибка удаления', error.response?.data?.detail || 'Не удалось удалить демон');
}
};
const handleEdit = (daemon) => {
setEditingDaemon(daemon);
setFormData({
name: daemon.name,
address: daemon.address,
port: daemon.port,
key: daemon.key,
remarks: daemon.remarks || ''
});
setShowAddModal(true);
};
const formatBytes = (bytes) => {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-400">Загрузка демонов...</div>
</div>
);
}
return (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<Server className="w-8 h-8 text-blue-400" />
<div>
<h2 className="text-2xl font-bold text-white">Демоны</h2>
<p className="text-gray-400">Управление удаленными серверами</p>
</div>
</div>
<div className="flex gap-2">
<button
onClick={loadDaemons}
className="btn-secondary flex items-center gap-2"
>
<RefreshCw className="w-4 h-4" />
Обновить
</button>
<button
onClick={() => {
setEditingDaemon(null);
setFormData({ name: '', address: '', port: 24444, key: '', remarks: '' });
setShowAddModal(true);
}}
className="btn-primary flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Добавить демон
</button>
</div>
</div>
{daemons.length === 0 ? (
<div className="card p-12 text-center">
<Server className="w-16 h-16 mx-auto mb-4 text-gray-500 opacity-50" />
<p className="text-lg font-medium mb-2">Нет демонов</p>
<p className="text-sm text-gray-400 mb-4">
Добавьте первый демон для управления удаленными серверами
</p>
<button
onClick={() => setShowAddModal(true)}
className="btn-primary"
>
Добавить демон
</button>
</div>
) : (
<div className="grid gap-4">
{daemons.map((daemon) => (
<div key={daemon.id} className="card p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-4">
<div className={`p-3 rounded-xl ${daemon.status === 'online' ? 'bg-green-500/20' : 'bg-red-500/20'}`}>
<Server className={`w-6 h-6 ${daemon.status === 'online' ? 'text-green-400' : 'text-red-400'}`} />
</div>
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-semibold text-white">{daemon.name}</h3>
{daemon.status === 'online' ? (
<span className="px-2 py-1 bg-green-500/20 text-green-400 text-xs rounded flex items-center gap-1">
<CheckCircle className="w-3 h-3" />
Онлайн
</span>
) : (
<span className="px-2 py-1 bg-red-500/20 text-red-400 text-xs rounded flex items-center gap-1">
<XCircle className="w-3 h-3" />
Оффлайн
</span>
)}
</div>
<p className="text-sm text-gray-400 mt-1">
{daemon.address}:{daemon.port}
</p>
{daemon.remarks && (
<p className="text-sm text-gray-500 mt-1">{daemon.remarks}</p>
)}
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => handleEdit(daemon)}
className="p-2 bg-dark-700 hover:bg-dark-600 rounded transition"
title="Редактировать"
>
<Edit className="w-4 h-4" />
</button>
<button
onClick={() => handleDelete(daemon.id)}
className="p-2 bg-dark-700 hover:bg-dark-600 rounded text-red-400 transition"
title="Удалить"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
{daemon.status === 'online' && daemon.system && (
<div className="grid grid-cols-3 gap-4 mt-4 pt-4 border-t border-gray-700">
<div>
<div className="flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-blue-400" />
<span className="text-sm text-gray-400">CPU</span>
</div>
<div className="text-xl font-bold text-white">{daemon.system.cpu_usage?.toFixed(1)}%</div>
<div className="w-full bg-dark-700 rounded-full h-2 mt-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min(daemon.system.cpu_usage || 0, 100)}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-green-400" />
<span className="text-sm text-gray-400">ОЗУ</span>
</div>
<div className="text-xl font-bold text-white">{daemon.system.memory_percent?.toFixed(1)}%</div>
<div className="text-xs text-gray-500">
{formatBytes(daemon.system.memory_used)} / {formatBytes(daemon.system.memory_total)}
</div>
<div className="w-full bg-dark-700 rounded-full h-2 mt-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min(daemon.system.memory_percent || 0, 100)}%` }}
/>
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<Activity className="w-4 h-4 text-purple-400" />
<span className="text-sm text-gray-400">Диск</span>
</div>
<div className="text-xl font-bold text-white">{daemon.system.disk_percent?.toFixed(1)}%</div>
<div className="text-xs text-gray-500">
{formatBytes(daemon.system.disk_used)} / {formatBytes(daemon.system.disk_total)}
</div>
<div className="w-full bg-dark-700 rounded-full h-2 mt-2">
<div
className="bg-purple-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min(daemon.system.disk_percent || 0, 100)}%` }}
/>
</div>
</div>
</div>
)}
{daemon.status === 'online' && daemon.servers && (
<div className="mt-4 pt-4 border-t border-gray-700">
<div className="flex items-center gap-2 text-sm text-gray-400">
<Server className="w-4 h-4" />
<span>Серверов: {daemon.servers.total || 0}</span>
<span className="text-gray-600"></span>
<span className="text-green-400">Запущено: {daemon.servers.running || 0}</span>
</div>
</div>
)}
</div>
))}
</div>
)}
{/* Модальное окно добавления/редактирования */}
{showAddModal && (
<div className="modal-overlay" onClick={() => setShowAddModal(false)}>
<div className="modal-content max-w-2xl" onClick={(e) => e.stopPropagation()}>
<h2 className="text-xl font-bold text-white mb-6">
{editingDaemon ? 'Редактировать демон' : 'Добавить демон'}
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2 text-white">
Название
</label>
<input
type="text"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
className="input"
placeholder="Main Server"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-2 text-white">
IP адрес
</label>
<input
type="text"
required
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
className="input"
placeholder="192.168.1.100"
/>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">
Порт
</label>
<input
type="number"
required
value={formData.port}
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) })}
className="input"
placeholder="24444"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">
Ключ демона
</label>
<input
type="text"
required
value={formData.key}
onChange={(e) => setFormData({ ...formData, key: e.target.value })}
className="input"
placeholder="your-secret-key"
/>
<p className="text-xs text-gray-400 mt-1">
Ключ должен совпадать с DAEMON_KEY в .env файле демона
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2 text-white">
Примечания (необязательно)
</label>
<textarea
value={formData.remarks}
onChange={(e) => setFormData({ ...formData, remarks: e.target.value })}
className="w-full bg-dark-800 border-gray-700 border rounded-xl px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-blue-500 transition resize-none"
rows={3}
placeholder="Дополнительная информация о демоне"
/>
</div>
<div className="flex gap-2 pt-4">
<button
type="button"
onClick={() => {
setShowAddModal(false);
setEditingDaemon(null);
}}
className="flex-1 btn-secondary"
>
Отмена
</button>
<button
type="submit"
className="flex-1 btn-primary"
>
{editingDaemon ? 'Сохранить' : 'Добавить'}
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { X, Save } from 'lucide-react'; import { X, Save } from 'lucide-react';
export default function FileEditorModal({ file, onClose, onSave, theme }) { export default function FileEditorModal({ file, onClose, onSave }) {
const [content, setContent] = useState(file.content); const [content, setContent] = useState(file.content);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -25,9 +25,9 @@ export default function FileEditorModal({ file, onClose, onSave, theme }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className={`${theme.secondary} rounded-lg w-full max-w-4xl h-[80vh] flex flex-col ${theme.border} border`}> <div className="bg-dark-800 rounded-lg w-full max-w-4xl h-[80vh] flex flex-col border border-gray-700">
<div className={`flex items-center justify-between p-4 ${theme.border} border-b`}> <div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className={`text-xl font-bold ${theme.text}`}>Редактирование: {file.name}</h2> <h2 className="text-xl font-bold text-white">Редактирование: {file.name}</h2>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={handleSave} onClick={handleSave}
@@ -39,23 +39,23 @@ export default function FileEditorModal({ file, onClose, onSave, theme }) {
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className={`${theme.textSecondary} hover:${theme.text} transition`} className="text-gray-400 hover:text-white transition"
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
</div> </div>
</div> </div>
<div className={`flex-1 overflow-hidden p-4 ${theme.console || theme.primary}`}> <div className="flex-1 overflow-hidden p-4 bg-dark-900">
<textarea <textarea
value={content} value={content}
onChange={(e) => setContent(e.target.value)} onChange={(e) => setContent(e.target.value)}
className={`w-full h-full ${theme.console || theme.primary} ${theme.consoleText || theme.text} font-mono text-sm p-4 rounded ${theme.border} border focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none`} className="w-full h-full bg-dark-900 text-gray-100 font-mono text-sm p-4 rounded border border-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none"
spellCheck={false} spellCheck={false}
/> />
</div> </div>
<div className={`p-4 ${theme.border} border-t text-sm ${theme.textSecondary}`}> <div className="p-4 border-t border-gray-700 text-sm text-gray-400">
Используйте Ctrl+S для быстрого сохранения Используйте Ctrl+S для быстрого сохранения
</div> </div>
</div> </div>

View File

@@ -6,7 +6,7 @@ import FileViewerModal from './FileViewerModal';
import { API_URL } from '../config'; import { API_URL } from '../config';
import { notify } from './NotificationSystem'; import { notify } from './NotificationSystem';
export default function FileManager({ serverName, token, theme }) { export default function FileManager({ serverName, token }) {
const [files, setFiles] = useState([]); const [files, setFiles] = useState([]);
const [currentPath, setCurrentPath] = useState(''); const [currentPath, setCurrentPath] = useState('');
const [editingFile, setEditingFile] = useState(null); const [editingFile, setEditingFile] = useState(null);
@@ -354,26 +354,26 @@ export default function FileManager({ serverName, token, theme }) {
}; };
return ( return (
<div className={`h-full flex flex-col ${theme.primary}`}> <div className="h-full flex flex-col bg-dark-900">
{/* Header */} {/* Header */}
<div className={`${theme.border} border-b p-4`}> <div className="border-b border-gray-700 p-4">
<h2 className={`text-xl font-semibold mb-4 ${theme.text}`}>Управление файлами</h2> <h2 className="text-xl font-semibold mb-4 text-white">Управление файлами</h2>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{/* Search */} {/* Search */}
<div className="flex-1 relative"> <div className="flex-1 relative">
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 ${theme.textSecondary}`} /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input <input
type="text" type="text"
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Поиск по названию ф..." placeholder="Поиск по названию ф..."
className={`w-full ${theme.input} ${theme.border} border rounded-lg pl-10 pr-4 py-2 ${theme.text} placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500`} className="w-full bg-dark-800 border-gray-700 border rounded-lg pl-10 pr-4 py-2 text-white placeholder:text-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
{/* Buttons */} {/* Buttons */}
<label className={`${theme.success} ${theme.successHover} px-4 py-2 rounded-lg cursor-pointer flex items-center gap-2 text-white font-medium transition shadow-lg`}> <label className="bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg cursor-pointer flex items-center gap-2 text-white font-medium transition shadow-lg">
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
Загрузить Загрузить
<input type="file" onChange={uploadFile} className="hidden" /> <input type="file" onChange={uploadFile} className="hidden" />
@@ -381,7 +381,7 @@ export default function FileManager({ serverName, token, theme }) {
<button <button
onClick={loadFiles} onClick={loadFiles}
className={`${theme.danger} ${theme.dangerHover} px-4 py-2 rounded-lg flex items-center gap-2 text-white font-medium transition shadow-lg`} className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded-lg flex items-center gap-2 text-white font-medium transition shadow-lg"
> >
Обновить Обновить
</button> </button>
@@ -436,14 +436,14 @@ export default function FileManager({ serverName, token, theme }) {
</button> </button>
{showNewMenu && ( {showNewMenu && (
<div className={`absolute right-0 mt-2 w-48 ${theme.secondary} rounded-lg shadow-xl ${theme.border} border z-10`}> <div className="absolute right-0 mt-2 w-48 bg-dark-800 rounded-lg shadow-xl border-gray-700 border z-10">
<button <button
onClick={() => { onClick={() => {
setCreatingNew('file'); setCreatingNew('file');
setShowNewMenu(false); setShowNewMenu(false);
setNewItemName(''); setNewItemName('');
}} }}
className={`w-full text-left px-4 py-2 ${theme.hover} ${theme.text} transition rounded-t-lg`} className="w-full text-left px-4 py-2 hover:bg-dark-700 text-white transition rounded-t-lg"
> >
📄 Создать файл 📄 Создать файл
</button> </button>
@@ -453,7 +453,7 @@ export default function FileManager({ serverName, token, theme }) {
setShowNewMenu(false); setShowNewMenu(false);
setNewItemName(''); setNewItemName('');
}} }}
className={`w-full text-left px-4 py-2 ${theme.hover} ${theme.text} transition rounded-b-lg`} className="w-full text-left px-4 py-2 hover:bg-dark-700 text-white transition rounded-b-lg"
> >
📁 Создать папку 📁 Создать папку
</button> </button>
@@ -464,17 +464,17 @@ export default function FileManager({ serverName, token, theme }) {
</div> </div>
{/* Path */} {/* Path */}
<div className={`${theme.secondary} px-4 py-3 ${theme.border} border-b`}> <div className="bg-dark-800 px-4 py-3 border-gray-700 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{currentPath && ( {currentPath && (
<button <button
onClick={goBack} onClick={goBack}
className={`${theme.hover} px-3 py-1 rounded text-sm ${theme.text} transition`} className="hover:bg-dark-700 px-3 py-1 rounded text-sm text-white transition"
> >
Назад Назад
</button> </button>
)} )}
<span className={`${theme.textSecondary} font-mono text-sm`}> <span className="text-gray-400 font-mono text-sm">
/{currentPath || ''} /{currentPath || ''}
</span> </span>
@@ -493,9 +493,9 @@ export default function FileManager({ serverName, token, theme }) {
{/* Table */} {/* Table */}
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto">
<table className="w-full"> <table className="w-full">
<thead className={`${theme.secondary} sticky top-0 ${theme.border} border-b`}> <thead className="bg-dark-800 sticky top-0 border-gray-700 border-b">
<tr> <tr>
<th className={`text-left p-4 ${theme.textSecondary} font-medium text-sm`}> <th className="text-left p-4 text-gray-400 font-medium text-sm">
<input <input
type="checkbox" type="checkbox"
className="mr-3 cursor-pointer" className="mr-3 cursor-pointer"
@@ -504,17 +504,17 @@ export default function FileManager({ serverName, token, theme }) {
/> />
Имя Имя
</th> </th>
<th className={`text-left p-4 ${theme.textSecondary} font-medium text-sm`}>Тип</th> <th className="text-left p-4 text-gray-400 font-medium text-sm">Тип</th>
<th className={`text-left p-4 ${theme.textSecondary} font-medium text-sm`}>Размер</th> <th className="text-left p-4 text-gray-400 font-medium text-sm">Размер</th>
<th className={`text-left p-4 ${theme.textSecondary} font-medium text-sm`}>Последнее изменение</th> <th className="text-left p-4 text-gray-400 font-medium text-sm">Последнее изменение</th>
<th className={`text-left p-4 ${theme.textSecondary} font-medium text-sm`}>Разрешение</th> <th className="text-left p-4 text-gray-400 font-medium text-sm">Разрешение</th>
<th className={`text-right p-4 ${theme.textSecondary} font-medium text-sm`}>Действия</th> <th className="text-right p-4 text-gray-400 font-medium text-sm">Действия</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{/* Форма создания нового файла/папки */} {/* Форма создания нового файла/папки */}
{creatingNew && ( {creatingNew && (
<tr className={`${theme.border} border-b bg-blue-900 bg-opacity-20`}> <tr className="border-gray-700 border-b bg-blue-900 bg-opacity-20">
<td className="p-4" colSpan="6"> <td className="p-4" colSpan="6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{creatingNew === 'file' ? ( {creatingNew === 'file' ? (
@@ -537,7 +537,7 @@ export default function FileManager({ serverName, token, theme }) {
}} }}
placeholder={creatingNew === 'file' ? 'Имя файла...' : 'Имя папки...'} placeholder={creatingNew === 'file' ? 'Имя файла...' : 'Имя папки...'}
autoFocus autoFocus
className={`flex-1 ${theme.input} ${theme.border} border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`} className="flex-1 bg-dark-800 border-gray-700 border rounded px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
<button <button
onClick={creatingNew === 'file' ? createNewFile : createNewFolder} onClick={creatingNew === 'file' ? createNewFile : createNewFolder}
@@ -550,7 +550,7 @@ export default function FileManager({ serverName, token, theme }) {
setCreatingNew(null); setCreatingNew(null);
setNewItemName(''); setNewItemName('');
}} }}
className={`${theme.danger} ${theme.dangerHover} px-4 py-2 rounded text-sm text-white transition`} className="bg-red-600 hover:bg-red-700 px-4 py-2 rounded text-sm text-white transition"
> >
Отмена Отмена
</button> </button>
@@ -562,7 +562,7 @@ export default function FileManager({ serverName, token, theme }) {
{filteredFiles.length === 0 ? ( {filteredFiles.length === 0 ? (
<tr> <tr>
<td colSpan="6" className="text-center py-12"> <td colSpan="6" className="text-center py-12">
<div className={theme.textSecondary}> <div className="text-gray-400">
<Folder className="w-12 h-12 mx-auto mb-2 opacity-50" /> <Folder className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p>No data</p> <p>No data</p>
</div> </div>
@@ -575,7 +575,7 @@ export default function FileManager({ serverName, token, theme }) {
return ( return (
<tr <tr
key={file.name} key={file.name}
className={`${theme.border} border-b ${theme.hover} transition ${ className={`border-gray-700 border-b hover:bg-dark-700 transition ${
isCut ? 'opacity-50 bg-orange-900 bg-opacity-20' : '' isCut ? 'opacity-50 bg-orange-900 bg-opacity-20' : ''
}`} }`}
> >
@@ -603,7 +603,7 @@ export default function FileManager({ serverName, token, theme }) {
if (e.key === 'Escape') setRenamingFile(null); if (e.key === 'Escape') setRenamingFile(null);
}} }}
autoFocus autoFocus
className={`${theme.input} ${theme.border} border rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`} className="bg-dark-800 border-gray-700 border rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
/> />
</div> </div>
) : ( ) : (
@@ -623,37 +623,37 @@ export default function FileManager({ serverName, token, theme }) {
) : ( ) : (
<File className="w-5 h-5 text-gray-400" /> <File className="w-5 h-5 text-gray-400" />
)} )}
<span className={theme.text}>{file.name}</span> <span className="text-white">{file.name}</span>
</div> </div>
)} )}
</td> </td>
<td className={`p-4 ${theme.textSecondary} text-sm`}> <td className="p-4 text-gray-400 text-sm">
{file.type === 'directory' ? 'Папка' : 'Файл'} {file.type === 'directory' ? 'Папка' : 'Файл'}
</td> </td>
<td className={`p-4 ${theme.textSecondary} text-sm`}>{formatSize(file.size)}</td> <td className="p-4 text-gray-400 text-sm">{formatSize(file.size)}</td>
<td className={`p-4 ${theme.textSecondary} text-sm`}>-</td> <td className="p-4 text-gray-400 text-sm">-</td>
<td className={`p-4 ${theme.textSecondary} text-sm`}>-</td> <td className="p-4 text-gray-400 text-sm">-</td>
<td className="p-4"> <td className="p-4">
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
{file.type === 'file' && ( {file.type === 'file' && (
<> <>
<button <button
onClick={() => viewFile(file.name)} onClick={() => viewFile(file.name)}
className={`${theme.card} ${theme.hover} p-2 rounded transition`} className="bg-dark-800 hover:bg-dark-700 p-2 rounded transition"
title="Просмотр" title="Просмотр"
> >
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => editFile(file.name)} onClick={() => editFile(file.name)}
className={`${theme.card} ${theme.hover} p-2 rounded transition`} className="bg-dark-800 hover:bg-dark-700 p-2 rounded transition"
title="Редактировать" title="Редактировать"
> >
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</button> </button>
<button <button
onClick={() => downloadFile(file.name)} onClick={() => downloadFile(file.name)}
className={`${theme.card} ${theme.hover} p-2 rounded transition`} className="bg-dark-800 hover:bg-dark-700 p-2 rounded transition"
title="Скачать" title="Скачать"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
@@ -662,7 +662,7 @@ export default function FileManager({ serverName, token, theme }) {
)} )}
<button <button
onClick={() => deleteFile(file.name)} onClick={() => deleteFile(file.name)}
className={`${theme.card} ${theme.hover} p-2 rounded text-red-400 transition`} className="bg-dark-800 hover:bg-dark-700 p-2 rounded text-red-400 transition"
title="Удалить" title="Удалить"
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
@@ -685,7 +685,6 @@ export default function FileManager({ serverName, token, theme }) {
setEditingFile(viewingFile); setEditingFile(viewingFile);
setViewingFile(null); setViewingFile(null);
}} }}
theme={theme}
/> />
)} )}
@@ -694,7 +693,6 @@ export default function FileManager({ serverName, token, theme }) {
file={editingFile} file={editingFile}
onClose={() => setEditingFile(null)} onClose={() => setEditingFile(null)}
onSave={saveFile} onSave={saveFile}
theme={theme}
/> />
)} )}
</div> </div>

View File

@@ -1,11 +1,11 @@
import { X, Edit } from 'lucide-react'; import { X, Edit } from 'lucide-react';
export default function FileViewerModal({ file, onClose, onEdit, theme }) { export default function FileViewerModal({ file, onClose, onEdit }) {
return ( return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className={`${theme.secondary} rounded-lg w-full max-w-4xl h-[80vh] flex flex-col ${theme.border} border`}> <div className="bg-dark-800 rounded-lg w-full max-w-4xl h-[80vh] flex flex-col border border-gray-700">
<div className={`flex items-center justify-between p-4 ${theme.border} border-b`}> <div className="flex items-center justify-between p-4 border-b border-gray-700">
<h2 className={`text-xl font-bold ${theme.text}`}>{file.name}</h2> <h2 className="text-xl font-bold text-white">{file.name}</h2>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={onEdit} onClick={onEdit}
@@ -16,15 +16,15 @@ export default function FileViewerModal({ file, onClose, onEdit, theme }) {
</button> </button>
<button <button
onClick={onClose} onClick={onClose}
className={`${theme.textSecondary} hover:${theme.text} transition`} className="text-gray-400 hover:text-white transition"
> >
<X className="w-6 h-6" /> <X className="w-6 h-6" />
</button> </button>
</div> </div>
</div> </div>
<div className={`flex-1 overflow-auto p-4 ${theme.console || theme.primary}`}> <div className="flex-1 overflow-auto p-4 bg-dark-900">
<pre className={`text-sm ${theme.consoleText || theme.text} font-mono whitespace-pre-wrap`}> <pre className="text-sm text-gray-100 font-mono whitespace-pre-wrap">
{file.content} {file.content}
</pre> </pre>
</div> </div>

View File

@@ -4,7 +4,7 @@ import axios from 'axios';
import { API_URL } from '../config'; import { API_URL } from '../config';
import { notify } from './NotificationSystem'; import { notify } from './NotificationSystem';
export default function Profile({ token, user, theme, onUsernameChange, viewingUsername }) { export default function Profile({ token, user, onUsernameChange, viewingUsername }) {
const [stats, setStats] = useState(null); const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview'); const [activeTab, setActiveTab] = useState('overview');
@@ -64,14 +64,11 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
// Обновляем токен
localStorage.setItem('token', data.access_token); localStorage.setItem('token', data.access_token);
notify('success', 'Имя изменено', `Ваше новое имя: ${data.username}`); notify('success', 'Имя изменено', `Ваше новое имя: ${data.username}`);
alert('Имя пользователя успешно изменено!'); alert('Имя пользователя успешно изменено!');
setUsernameForm({ new_username: '', password: '' }); setUsernameForm({ new_username: '', password: '' });
// Уведомляем родительский компонент
if (onUsernameChange) { if (onUsernameChange) {
onUsernameChange(data.access_token, data.username); onUsernameChange(data.access_token, data.username);
} }
@@ -127,63 +124,53 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
const getRoleName = (role) => { const getRoleName = (role) => {
switch (role) { switch (role) {
case 'admin': case 'owner': return 'Владелец';
return 'Администратор'; case 'admin': return 'Администратор';
case 'support': case 'support': return 'Тех. поддержка';
return 'Тех. поддержка'; case 'banned': return 'Забанен';
case 'banned': default: return 'Пользователь';
return 'Забанен';
default:
return 'Пользователь';
} }
}; };
const getRoleColor = (role) => { const getRoleColor = (role) => {
switch (role) { switch (role) {
case 'admin': case 'owner': return 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50';
return 'bg-blue-500/20 text-blue-500 border-blue-500/50'; case 'admin': return 'bg-blue-500/20 text-blue-500 border-blue-500/50';
case 'support': case 'support': return 'bg-purple-500/20 text-purple-500 border-purple-500/50';
return 'bg-purple-500/20 text-purple-500 border-purple-500/50'; case 'banned': return 'bg-red-500/20 text-red-500 border-red-500/50';
case 'banned': default: return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
return 'bg-red-500/20 text-red-500 border-red-500/50';
default:
return 'bg-gray-500/20 text-gray-500 border-gray-500/50';
} }
}; };
if (loading) { if (loading) {
return ( return (
<div className={`h-full ${theme.primary} ${theme.text} flex items-center justify-center`}> <div className="h-full bg-dark-900 text-white flex items-center justify-center">
<div className="text-center"> <div className="text-center">
<div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <div className="w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className={theme.textSecondary}>Загрузка профиля...</p> <p className="text-gray-400">Загрузка профиля...</p>
</div> </div>
</div> </div>
); );
} }
return ( return (
<div className={`h-full ${theme.primary} ${theme.text} p-6 overflow-y-auto`}> <div className="h-full bg-dark-900 text-white p-6 overflow-y-auto">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-6"> <div className="mb-6">
<h1 className="text-2xl font-bold mb-2"> <h1 className="text-2xl font-bold mb-2">
{isViewingOther ? `Профиль пользователя: ${viewingUsername}` : 'Личный кабинет'} {isViewingOther ? `Профиль пользователя: ${viewingUsername}` : 'Личный кабинет'}
</h1> </h1>
<p className={theme.textSecondary}> <p className="text-gray-400">
{isViewingOther ? 'Просмотр профиля другого пользователя' : 'Управление профилем и настройками'} {isViewingOther ? 'Просмотр профиля другого пользователя' : 'Управление профилем и настройками'}
</p> </p>
</div> </div>
{/* Tabs */}
{!isViewingOther && ( {!isViewingOther && (
<div className={`${theme.secondary} ${theme.border} border rounded-2xl mb-6 p-2 flex gap-2`}> <div className="card mb-6 p-2 flex gap-2">
<button <button
onClick={() => setActiveTab('overview')} onClick={() => setActiveTab('overview')}
className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${ className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${
activeTab === 'overview' activeTab === 'overview' ? 'bg-primary-600 text-white' : 'hover:bg-dark-700'
? `${theme.accent} text-white`
: `${theme.hover}`
}`} }`}
> >
<TrendingUp className="w-4 h-4 inline mr-2" /> <TrendingUp className="w-4 h-4 inline mr-2" />
@@ -192,9 +179,7 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
<button <button
onClick={() => setActiveTab('username')} onClick={() => setActiveTab('username')}
className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${ className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${
activeTab === 'username' activeTab === 'username' ? 'bg-primary-600 text-white' : 'hover:bg-dark-700'
? `${theme.accent} text-white`
: `${theme.hover}`
}`} }`}
> >
<User className="w-4 h-4 inline mr-2" /> <User className="w-4 h-4 inline mr-2" />
@@ -203,9 +188,7 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
<button <button
onClick={() => setActiveTab('password')} onClick={() => setActiveTab('password')}
className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${ className={`flex-1 px-4 py-3 rounded-xl font-medium transition ${
activeTab === 'password' activeTab === 'password' ? 'bg-primary-600 text-white' : 'hover:bg-dark-700'
? `${theme.accent} text-white`
: `${theme.hover}`
}`} }`}
> >
<Lock className="w-4 h-4 inline mr-2" /> <Lock className="w-4 h-4 inline mr-2" />
@@ -214,13 +197,11 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
</div> </div>
)} )}
{/* Overview Tab */}
{(activeTab === 'overview' || isViewingOther) && ( {(activeTab === 'overview' || isViewingOther) && (
<div className="space-y-6"> <div className="space-y-6">
{/* User Info Card */} <div className="card p-6">
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}>
<div className="flex items-center gap-4 mb-6"> <div className="flex items-center gap-4 mb-6">
<div className={`${theme.accent} p-4 rounded-2xl`}> <div className="bg-primary-600 p-4 rounded-2xl">
<User className="w-8 h-8 text-white" /> <User className="w-8 h-8 text-white" />
</div> </div>
<div> <div>
@@ -233,39 +214,36 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
</div> </div>
</div> </div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* Servers */} <div className="card p-6">
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className={`${theme.accent} p-3 rounded-xl`}> <div className="bg-primary-600 p-3 rounded-xl">
<Server className="w-6 h-6 text-white" /> <Server className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<p className={`text-sm ${theme.textSecondary}`}>Всего серверов</p> <p className="text-sm text-gray-400">Всего серверов</p>
<p className="text-2xl font-bold">{stats?.total_servers || 0}</p> <p className="text-2xl font-bold">{stats?.total_servers || 0}</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className={theme.textSecondary}>Мои серверы:</span> <span className="text-gray-400">Мои серверы:</span>
<span className="font-medium">{stats?.owned_servers?.length || 0}</span> <span className="font-medium">{stats?.owned_servers?.length || 0}</span>
</div> </div>
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className={theme.textSecondary}>Доступные:</span> <span className="text-gray-400">Доступные:</span>
<span className="font-medium">{stats?.accessible_servers?.length || 0}</span> <span className="font-medium">{stats?.accessible_servers?.length || 0}</span>
</div> </div>
</div> </div>
</div> </div>
{/* Tickets */} <div className="card p-6">
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className={`${theme.accent} p-3 rounded-xl`}> <div className="bg-primary-600 p-3 rounded-xl">
<MessageSquare className="w-6 h-6 text-white" /> <MessageSquare className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<p className={`text-sm ${theme.textSecondary}`}>Мои тикеты</p> <p className="text-sm text-gray-400">Мои тикеты</p>
<p className="text-2xl font-bold">{stats?.tickets?.total || 0}</p> <p className="text-2xl font-bold">{stats?.tickets?.total || 0}</p>
</div> </div>
</div> </div>
@@ -285,18 +263,18 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
</div> </div>
</div> </div>
{/* Role Info */} <div className="card p-6">
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className={`${theme.accent} p-3 rounded-xl`}> <div className="bg-primary-600 p-3 rounded-xl">
<Shield className="w-6 h-6 text-white" /> <Shield className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<p className={`text-sm ${theme.textSecondary}`}>Ваша роль</p> <p className="text-sm text-gray-400">Ваша роль</p>
<p className="text-xl font-bold">{getRoleName(stats?.role)}</p> <p className="text-xl font-bold">{getRoleName(stats?.role)}</p>
</div> </div>
</div> </div>
<div className={`text-sm ${theme.textSecondary}`}> <div className="text-sm text-gray-400">
{stats?.role === 'owner' && '👑 Владелец панели - полный контроль над всеми функциями'}
{stats?.role === 'admin' && 'Полный доступ ко всем функциям панели'} {stats?.role === 'admin' && 'Полный доступ ко всем функциям панели'}
{stats?.role === 'support' && 'Доступ к системе тикетов и поддержке'} {stats?.role === 'support' && 'Доступ к системе тикетов и поддержке'}
{stats?.role === 'user' && 'Доступ к своим серверам и тикетам'} {stats?.role === 'user' && 'Доступ к своим серверам и тикетам'}
@@ -305,21 +283,17 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
</div> </div>
</div> </div>
{/* Servers List */}
{stats?.owned_servers?.length > 0 && ( {stats?.owned_servers?.length > 0 && (
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6`}> <div className="card p-6">
<h3 className="text-lg font-bold mb-4">Мои серверы</h3> <h3 className="text-lg font-bold mb-4">Мои серверы</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
{stats.owned_servers.map((server) => ( {stats.owned_servers.map((server) => (
<div <div key={server.name} className="bg-dark-700 border border-gray-700 rounded-xl p-4">
key={server.name}
className={`${theme.tertiary} ${theme.border} border rounded-xl p-4`}
>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Server className="w-5 h-5" /> <Server className="w-5 h-5" />
<div> <div>
<p className="font-medium">{server.displayName}</p> <p className="font-medium">{server.displayName}</p>
<p className={`text-xs ${theme.textSecondary}`}>{server.name}</p> <p className="text-xs text-gray-400">{server.name}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -330,154 +304,115 @@ export default function Profile({ token, user, theme, onUsernameChange, viewingU
</div> </div>
)} )}
{/* Username Tab */}
{activeTab === 'username' && ( {activeTab === 'username' && (
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6 max-w-2xl mx-auto`}> <div className="card p-6 max-w-2xl mx-auto">
<h2 className="text-xl font-bold mb-6">Изменить имя пользователя</h2> <h2 className="text-xl font-bold mb-6">Изменить имя пользователя</h2>
<form onSubmit={handleUsernameChange} className="space-y-4"> <form onSubmit={handleUsernameChange} className="space-y-4">
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">Текущее имя пользователя</label>
Текущее имя пользователя <input type="text" value={stats?.username} disabled className="input cursor-not-allowed opacity-50" />
</label>
<input
type="text"
value={stats?.username}
disabled
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-3 ${theme.textSecondary} cursor-not-allowed`}
/>
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">Новое имя пользователя</label>
Новое имя пользователя
</label>
<input <input
type="text" type="text"
value={usernameForm.new_username} value={usernameForm.new_username}
onChange={(e) => setUsernameForm({ ...usernameForm, new_username: e.target.value })} onChange={(e) => setUsernameForm({ ...usernameForm, new_username: e.target.value })}
placeholder="Введите новое имя" placeholder="Введите новое имя"
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-3 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input"
/> />
<p className={`text-xs ${theme.textSecondary} mt-1`}> <p className="text-xs text-gray-400 mt-1">Минимум 3 символа</p>
Минимум 3 символа
</p>
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">Подтвердите паролем</label>
Подтвердите паролем
</label>
<div className="relative"> <div className="relative">
<input <input
type={showUsernamePassword ? 'text' : 'password'} type={showUsernamePassword ? 'text' : 'password'}
value={usernameForm.password} value={usernameForm.password}
onChange={(e) => setUsernameForm({ ...usernameForm, password: e.target.value })} onChange={(e) => setUsernameForm({ ...usernameForm, password: e.target.value })}
placeholder="Введите текущий пароль" placeholder="Введите текущий пароль"
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-3 pr-12 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input pr-12"
/> />
<button <button
type="button" type="button"
onClick={() => setShowUsernamePassword(!showUsernamePassword)} onClick={() => setShowUsernamePassword(!showUsernamePassword)}
className={`absolute right-3 top-1/2 -translate-y-1/2 ${theme.textSecondary} hover:${theme.text} transition`} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition"
> >
{showUsernamePassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {showUsernamePassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
</div> </div>
<div className="bg-dark-700 border border-gray-700 rounded-xl p-4">
<div className={`${theme.tertiary} ${theme.border} border rounded-xl p-4`}> <p className="text-sm text-gray-400">
<p className={`text-sm ${theme.textSecondary}`}>
После изменения имени пользователя вы будете автоматически перелогинены с новым именем. После изменения имени пользователя вы будете автоматически перелогинены с новым именем.
</p> </p>
</div> </div>
<button type="submit" disabled={usernameLoading} className="btn-primary w-full disabled:opacity-50">
<button
type="submit"
disabled={usernameLoading}
className={`w-full ${theme.accent} ${theme.accentHover} px-6 py-3 rounded-xl font-medium text-white transition disabled:opacity-50`}
>
{usernameLoading ? 'Изменение...' : 'Изменить имя пользователя'} {usernameLoading ? 'Изменение...' : 'Изменить имя пользователя'}
</button> </button>
</form> </form>
</div> </div>
)} )}
{/* Password Tab */}
{activeTab === 'password' && ( {activeTab === 'password' && (
<div className={`${theme.card} ${theme.border} border rounded-2xl p-6 max-w-2xl mx-auto`}> <div className="card p-6 max-w-2xl mx-auto">
<h2 className="text-xl font-bold mb-6">Изменить пароль</h2> <h2 className="text-xl font-bold mb-6">Изменить пароль</h2>
<form onSubmit={handlePasswordChange} className="space-y-4"> <form onSubmit={handlePasswordChange} className="space-y-4">
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">Текущий пароль</label>
Текущий пароль
</label>
<div className="relative"> <div className="relative">
<input <input
type={showOldPassword ? 'text' : 'password'} type={showOldPassword ? 'text' : 'password'}
value={passwordForm.old_password} value={passwordForm.old_password}
onChange={(e) => setPasswordForm({ ...passwordForm, old_password: e.target.value })} onChange={(e) => setPasswordForm({ ...passwordForm, old_password: e.target.value })}
placeholder="Введите текущий пароль" placeholder="Введите текущий пароль"
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-3 pr-12 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input pr-12"
/> />
<button <button
type="button" type="button"
onClick={() => setShowOldPassword(!showOldPassword)} onClick={() => setShowOldPassword(!showOldPassword)}
className={`absolute right-3 top-1/2 -translate-y-1/2 ${theme.textSecondary} hover:${theme.text} transition`} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition"
> >
{showOldPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {showOldPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">Новый пароль</label>
Новый пароль
</label>
<div className="relative"> <div className="relative">
<input <input
type={showNewPassword ? 'text' : 'password'} type={showNewPassword ? 'text' : 'password'}
value={passwordForm.new_password} value={passwordForm.new_password}
onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })} onChange={(e) => setPasswordForm({ ...passwordForm, new_password: e.target.value })}
placeholder="Введите новый пароль" placeholder="Введите новый пароль"
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-3 pr-12 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input pr-12"
/> />
<button <button
type="button" type="button"
onClick={() => setShowNewPassword(!showNewPassword)} onClick={() => setShowNewPassword(!showNewPassword)}
className={`absolute right-3 top-1/2 -translate-y-1/2 ${theme.textSecondary} hover:${theme.text} transition`} className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-white transition"
> >
{showNewPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />} {showNewPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
</button> </button>
</div> </div>
<p className={`text-xs ${theme.textSecondary} mt-1`}> <p className="text-xs text-gray-400 mt-1">Минимум 6 символов</p>
Минимум 6 символов
</p>
</div> </div>
<div> <div>
<label className={`block text-sm font-medium mb-2 ${theme.text}`}> <label className="block text-sm font-medium mb-2 text-white">Подтвердите новый пароль</label>
Подтвердите новый пароль
</label>
<input <input
type="password" type="password"
value={passwordForm.confirm_password} value={passwordForm.confirm_password}
onChange={(e) => setPasswordForm({ ...passwordForm, confirm_password: e.target.value })} onChange={(e) => setPasswordForm({ ...passwordForm, confirm_password: e.target.value })}
placeholder="Повторите новый пароль" placeholder="Повторите новый пароль"
className={`w-full ${theme.input} ${theme.border} border rounded-xl px-4 py-3 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input"
/> />
</div> </div>
<div className="bg-dark-700 border border-gray-700 rounded-xl p-4">
<div className={`${theme.tertiary} ${theme.border} border rounded-xl p-4`}> <p className="text-sm text-gray-400">
<p className={`text-sm ${theme.textSecondary}`}>
После изменения пароля используйте новый пароль для входа в систему. После изменения пароля используйте новый пароль для входа в систему.
</p> </p>
</div> </div>
<button type="submit" disabled={passwordLoading} className="btn-primary w-full disabled:opacity-50">
<button
type="submit"
disabled={passwordLoading}
className={`w-full ${theme.accent} ${theme.accentHover} px-6 py-3 rounded-xl font-medium text-white transition disabled:opacity-50`}
>
{passwordLoading ? 'Изменение...' : 'Изменить пароль'} {passwordLoading ? 'Изменение...' : 'Изменить пароль'}
</button> </button>
</form> </form>

View File

@@ -3,7 +3,7 @@ import { Cpu, HardDrive, Activity } from 'lucide-react';
import axios from 'axios'; import axios from 'axios';
import { API_URL } from '../config'; import { API_URL } from '../config';
export default function Stats({ serverName, token, theme }) { export default function Stats({ serverName, token }) {
const [stats, setStats] = useState({ const [stats, setStats] = useState({
status: 'stopped', status: 'stopped',
cpu: 0, cpu: 0,
@@ -29,17 +29,17 @@ export default function Stats({ serverName, token, theme }) {
}; };
return ( return (
<div className={`p-8 ${theme.primary}`}> <div className="p-8 bg-dark-900">
<h2 className={`text-2xl font-bold mb-6 ${theme.text}`}>Статистика сервера</h2> <h2 className="text-2xl font-bold mb-6 text-white">Статистика сервера</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className={`${theme.card} rounded-lg p-6 ${theme.border} border`}> <div className="card p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className={`text-lg font-semibold ${theme.text}`}>CPU</h3> <h3 className="text-lg font-semibold text-white">CPU</h3>
<Cpu className="w-6 h-6 text-blue-400" /> <Cpu className="w-6 h-6 text-blue-400" />
</div> </div>
<div className={`text-3xl font-bold mb-2 ${theme.text}`}>{stats.cpu}%</div> <div className="text-3xl font-bold mb-2 text-white">{stats.cpu}%</div>
<div className={`w-full ${theme.tertiary} rounded-full h-2`}> <div className="w-full bg-dark-700 rounded-full h-2">
<div <div
className="bg-blue-500 h-2 rounded-full transition-all" className="bg-blue-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min(stats.cpu, 100)}%` }} style={{ width: `${Math.min(stats.cpu, 100)}%` }}
@@ -47,13 +47,13 @@ export default function Stats({ serverName, token, theme }) {
</div> </div>
</div> </div>
<div className={`${theme.card} rounded-lg p-6 ${theme.border} border`}> <div className="card p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className={`text-lg font-semibold ${theme.text}`}>ОЗУ</h3> <h3 className="text-lg font-semibold text-white">ОЗУ</h3>
<Activity className="w-6 h-6 text-green-400" /> <Activity className="w-6 h-6 text-green-400" />
</div> </div>
<div className={`text-3xl font-bold mb-2 ${theme.text}`}>{stats.memory} МБ</div> <div className="text-3xl font-bold mb-2 text-white">{stats.memory} МБ</div>
<div className={`w-full ${theme.tertiary} rounded-full h-2`}> <div className="w-full bg-dark-700 rounded-full h-2">
<div <div
className="bg-green-500 h-2 rounded-full transition-all" className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${Math.min((stats.memory / 2048) * 100, 100)}%` }} style={{ width: `${Math.min((stats.memory / 2048) * 100, 100)}%` }}
@@ -61,27 +61,27 @@ export default function Stats({ serverName, token, theme }) {
</div> </div>
</div> </div>
<div className={`${theme.card} rounded-lg p-6 ${theme.border} border`}> <div className="card p-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className={`text-lg font-semibold ${theme.text}`}>Диск</h3> <h3 className="text-lg font-semibold text-white">Диск</h3>
<HardDrive className="w-6 h-6 text-purple-400" /> <HardDrive className="w-6 h-6 text-purple-400" />
</div> </div>
<div className={`text-3xl font-bold mb-2 ${theme.text}`}>{stats.disk} МБ</div> <div className="text-3xl font-bold mb-2 text-white">{stats.disk} МБ</div>
<div className={`text-sm ${theme.textSecondary} mt-2`}> <div className="text-sm text-gray-400 mt-2">
Использовано на диске Использовано на диске
</div> </div>
</div> </div>
</div> </div>
<div className={`mt-8 ${theme.card} rounded-lg p-6 ${theme.border} border`}> <div className="mt-8 card p-6">
<h3 className={`text-lg font-semibold mb-4 ${theme.text}`}>Статус</h3> <h3 className="text-lg font-semibold mb-4 text-white">Статус</h3>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div <div
className={`w-4 h-4 rounded-full ${ className={`w-4 h-4 rounded-full ${
stats.status === 'running' ? 'bg-green-500' : 'bg-red-500' stats.status === 'running' ? 'bg-green-500' : 'bg-red-500'
}`} }`}
/> />
<span className={`text-xl ${theme.text}`}> <span className="text-xl text-white">
{stats.status === 'running' ? 'Запущен' : 'Остановлен'} {stats.status === 'running' ? 'Запущен' : 'Остановлен'}
</span> </span>
</div> </div>

View File

@@ -4,7 +4,7 @@ import axios from 'axios';
import { API_URL } from '../config'; import { API_URL } from '../config';
import { notify } from './NotificationSystem'; import { notify } from './NotificationSystem';
export default function TicketChat({ ticket, token, user, theme, onBack }) { export default function TicketChat({ ticket, token, user, onBack }) {
const [messages, setMessages] = useState(ticket.messages || []); const [messages, setMessages] = useState(ticket.messages || []);
const [newMessage, setNewMessage] = useState(''); const [newMessage, setNewMessage] = useState('');
const [currentTicket, setCurrentTicket] = useState(ticket); const [currentTicket, setCurrentTicket] = useState(ticket);
@@ -150,22 +150,22 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
} }
}; };
const canChangeStatus = user.role === 'admin' || user.role === 'support'; const canChangeStatus = user.role === 'owner' || user.role === 'admin' || user.role === 'support';
return ( return (
<div className={`h-full flex flex-col ${theme.primary}`}> <div className="h-full flex flex-col bg-dark-900">
{/* Header */} {/* Header */}
<div className={`${theme.secondary} ${theme.border} border-b p-4`}> <div className="bg-dark-800 border-gray-700 border-b p-4">
<div className="flex items-center gap-4 mb-3"> <div className="flex items-center gap-4 mb-3">
<button <button
onClick={onBack} onClick={onBack}
className={`${theme.hover} p-2 rounded-lg transition`} className="hover:bg-dark-700 p-2 rounded-lg transition"
> >
<ArrowLeft className="w-5 h-5" /> <ArrowLeft className="w-5 h-5" />
</button> </button>
<div className="flex-1"> <div className="flex-1">
<h2 className="text-lg font-bold">{currentTicket.title}</h2> <h2 className="text-lg font-bold">{currentTicket.title}</h2>
<p className={`text-sm ${theme.textSecondary}`}> <p className="text-sm text-gray-400">
Автор: {currentTicket.author} Создан: {new Date(currentTicket.created_at).toLocaleString('ru-RU')} Автор: {currentTicket.author} Создан: {new Date(currentTicket.created_at).toLocaleString('ru-RU')}
</p> </p>
</div> </div>
@@ -184,7 +184,7 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
className={`flex-1 px-3 py-2 rounded-lg border transition ${ className={`flex-1 px-3 py-2 rounded-lg border transition ${
currentTicket.status === 'pending' currentTicket.status === 'pending'
? 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50' ? 'bg-yellow-500/20 text-yellow-500 border-yellow-500/50'
: `${theme.card} ${theme.hover} ${theme.border}` : 'bg-dark-800 hover:bg-dark-700 border-gray-700'
}`} }`}
> >
<Clock className="w-4 h-4 inline mr-2" /> <Clock className="w-4 h-4 inline mr-2" />
@@ -196,7 +196,7 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
className={`flex-1 px-3 py-2 rounded-lg border transition ${ className={`flex-1 px-3 py-2 rounded-lg border transition ${
currentTicket.status === 'in_progress' currentTicket.status === 'in_progress'
? 'bg-blue-500/20 text-blue-500 border-blue-500/50' ? 'bg-blue-500/20 text-blue-500 border-blue-500/50'
: `${theme.card} ${theme.hover} ${theme.border}` : 'bg-dark-800 hover:bg-dark-700 border-gray-700'
}`} }`}
> >
<AlertCircle className="w-4 h-4 inline mr-2" /> <AlertCircle className="w-4 h-4 inline mr-2" />
@@ -208,7 +208,7 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
className={`flex-1 px-3 py-2 rounded-lg border transition ${ className={`flex-1 px-3 py-2 rounded-lg border transition ${
currentTicket.status === 'closed' currentTicket.status === 'closed'
? 'bg-green-500/20 text-green-500 border-green-500/50' ? 'bg-green-500/20 text-green-500 border-green-500/50'
: `${theme.card} ${theme.hover} ${theme.border}` : 'bg-dark-800 hover:bg-dark-700 border-gray-700'
}`} }`}
> >
<CheckCircle className="w-4 h-4 inline mr-2" /> <CheckCircle className="w-4 h-4 inline mr-2" />
@@ -228,20 +228,20 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
<div <div
className={`max-w-[70%] rounded-2xl px-4 py-3 ${ className={`max-w-[70%] rounded-2xl px-4 py-3 ${
msg.author === 'system' msg.author === 'system'
? `${theme.tertiary} ${theme.border} border text-center` ? 'bg-dark-700 border-gray-700 border text-center'
: msg.author === user.username : msg.author === user.username
? `${theme.accent} text-white` ? 'bg-primary-600 text-white'
: `${theme.card} ${theme.border} border` : 'bg-dark-800 border-gray-700 border'
}`} }`}
> >
{msg.author !== 'system' && msg.author !== user.username && ( {msg.author !== 'system' && msg.author !== user.username && (
<div className={`text-xs font-semibold mb-1 ${theme.textSecondary}`}> <div className="text-xs font-semibold mb-1 text-gray-400">
{msg.author} {msg.author}
</div> </div>
)} )}
<div className="whitespace-pre-wrap break-words">{msg.text}</div> <div className="whitespace-pre-wrap break-words">{msg.text}</div>
<div className={`text-xs mt-1 ${ <div className={`text-xs mt-1 ${
msg.author === user.username ? 'text-white/70' : theme.textSecondary msg.author === user.username ? 'text-white/70' : 'text-gray-400'
}`}> }`}>
{new Date(msg.timestamp).toLocaleTimeString('ru-RU')} {new Date(msg.timestamp).toLocaleTimeString('ru-RU')}
</div> </div>
@@ -253,7 +253,7 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
{/* Input */} {/* Input */}
{currentTicket.status !== 'closed' && ( {currentTicket.status !== 'closed' && (
<form onSubmit={sendMessage} className={`${theme.border} border-t p-4`}> <form onSubmit={sendMessage} className="border-gray-700 border-t p-4">
<div className="flex gap-2"> <div className="flex gap-2">
<input <input
type="text" type="text"
@@ -261,12 +261,12 @@ export default function TicketChat({ ticket, token, user, theme, onBack }) {
onChange={(e) => setNewMessage(e.target.value)} onChange={(e) => setNewMessage(e.target.value)}
placeholder="Введите сообщение..." placeholder="Введите сообщение..."
disabled={loading} disabled={loading}
className={`flex-1 ${theme.input} ${theme.border} border rounded-xl px-4 py-3 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} className="input flex-1"
/> />
<button <button
type="submit" type="submit"
disabled={loading || !newMessage.trim()} disabled={loading || !newMessage.trim()}
className={`${theme.accent} ${theme.accentHover} px-6 py-3 rounded-xl flex items-center gap-2 text-white transition disabled:opacity-50`} className="btn-primary px-6 py-3 flex items-center gap-2 disabled:opacity-50"
> >
<Send className="w-4 h-4" /> <Send className="w-4 h-4" />
Отправить Отправить

View File

@@ -6,7 +6,7 @@ import TicketChat from './TicketChat';
import CreateTicketModal from './CreateTicketModal'; import CreateTicketModal from './CreateTicketModal';
import { notify } from './NotificationSystem'; import { notify } from './NotificationSystem';
export default function Tickets({ token, user, theme }) { export default function Tickets({ token, user }) {
const [tickets, setTickets] = useState([]); const [tickets, setTickets] = useState([]);
const [selectedTicket, setSelectedTicket] = useState(null); const [selectedTicket, setSelectedTicket] = useState(null);
const [showCreateModal, setShowCreateModal] = useState(false); const [showCreateModal, setShowCreateModal] = useState(false);
@@ -99,7 +99,6 @@ export default function Tickets({ token, user, theme }) {
ticket={selectedTicket} ticket={selectedTicket}
token={token} token={token}
user={user} user={user}
theme={theme}
onBack={() => { onBack={() => {
setSelectedTicket(null); setSelectedTicket(null);
loadTickets(); loadTickets();
@@ -109,17 +108,17 @@ export default function Tickets({ token, user, theme }) {
} }
return ( return (
<div className={`h-full ${theme.primary} ${theme.text} p-6`}> <div className="h-full bg-dark-900 text-white p-6">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div> <div>
<h1 className="text-2xl font-bold mb-2">Тикеты</h1> <h1 className="text-2xl font-bold mb-2">Тикеты</h1>
<p className={theme.textSecondary}>Система поддержки</p> <p className="text-gray-400">Система поддержки</p>
</div> </div>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className={`${theme.accent} ${theme.accentHover} px-4 py-2 rounded-xl flex items-center gap-2 text-white transition`} className="btn-primary flex items-center gap-2"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
Создать тикет Создать тикет
@@ -130,18 +129,18 @@ export default function Tickets({ token, user, theme }) {
{loading ? ( {loading ? (
<div className="text-center py-12"> <div className="text-center py-12">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className={theme.textSecondary}>Загрузка тикетов...</p> <p className="text-gray-400">Загрузка тикетов...</p>
</div> </div>
) : tickets.length === 0 ? ( ) : tickets.length === 0 ? (
<div className={`${theme.card} ${theme.border} border rounded-2xl p-12 text-center`}> <div className="card p-12 text-center">
<MessageSquare className={`w-16 h-16 mx-auto mb-4 ${theme.textSecondary} opacity-50`} /> <MessageSquare className="w-16 h-16 mx-auto mb-4 text-gray-500 opacity-50" />
<p className="text-lg font-medium mb-2">Нет тикетов</p> <p className="text-lg font-medium mb-2">Нет тикетов</p>
<p className={`text-sm ${theme.textSecondary} mb-4`}> <p className="text-sm text-gray-400 mb-4">
Создайте первый тикет для обращения в поддержку Создайте первый тикет для обращения в поддержку
</p> </p>
<button <button
onClick={() => setShowCreateModal(true)} onClick={() => setShowCreateModal(true)}
className={`${theme.accent} ${theme.accentHover} px-6 py-2 rounded-xl text-white transition`} className="btn-primary"
> >
Создать тикет Создать тикет
</button> </button>
@@ -152,12 +151,12 @@ export default function Tickets({ token, user, theme }) {
<div <div
key={ticket.id} key={ticket.id}
onClick={() => setSelectedTicket(ticket)} onClick={() => setSelectedTicket(ticket)}
className={`${theme.card} ${theme.border} border rounded-2xl p-6 cursor-pointer ${theme.hover} transition-all duration-200`} className="card p-6 cursor-pointer hover:border-gray-600 transition-all duration-200"
> >
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex-1"> <div className="flex-1">
<h3 className="text-lg font-semibold mb-2">{ticket.title}</h3> <h3 className="text-lg font-semibold mb-2">{ticket.title}</h3>
<p className={`text-sm ${theme.textSecondary} line-clamp-2`}> <p className="text-sm text-gray-400 line-clamp-2">
{ticket.description} {ticket.description}
</p> </p>
</div> </div>
@@ -167,15 +166,15 @@ export default function Tickets({ token, user, theme }) {
</div> </div>
</div> </div>
<div className="flex items-center gap-4 text-sm"> <div className="flex items-center gap-4 text-sm">
<span className={theme.textSecondary}> <span className="text-gray-400">
Автор: <span className={theme.text}>{ticket.author}</span> Автор: <span className="text-white">{ticket.author}</span>
</span> </span>
<span className={theme.textSecondary}></span> <span className="text-gray-400"></span>
<span className={theme.textSecondary}> <span className="text-gray-400">
Сообщений: <span className={theme.text}>{ticket.messages?.length || 0}</span> Сообщений: <span className="text-white">{ticket.messages?.length || 0}</span>
</span> </span>
<span className={theme.textSecondary}></span> <span className="text-gray-400"></span>
<span className={theme.textSecondary}> <span className="text-gray-400">
{new Date(ticket.created_at).toLocaleString('ru-RU')} {new Date(ticket.created_at).toLocaleString('ru-RU')}
</span> </span>
</div> </div>
@@ -188,7 +187,6 @@ export default function Tickets({ token, user, theme }) {
{showCreateModal && ( {showCreateModal && (
<CreateTicketModal <CreateTicketModal
token={token} token={token}
theme={theme}
onClose={() => setShowCreateModal(false)} onClose={() => setShowCreateModal(false)}
onCreated={handleTicketCreated} onCreated={handleTicketCreated}
/> />

View File

@@ -1,8 +1,9 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Users, Shield, Ban, Trash2, UserCheck, Server, AlertCircle, CheckCircle } from 'lucide-react'; import { Users, Shield, Ban, Trash2, UserCheck, Server } from 'lucide-react';
import axios from 'axios'; import axios from 'axios';
import { notify } from './NotificationSystem';
const UserManagement = ({ currentUser, addNotification }) => { const UserManagement = ({ token, currentUser }) => {
const [users, setUsers] = useState([]); const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedUser, setSelectedUser] = useState(null); const [selectedUser, setSelectedUser] = useState(null);
@@ -14,7 +15,6 @@ const UserManagement = ({ currentUser, addNotification }) => {
// Загрузка пользователей // Загрузка пользователей
const loadUsers = async () => { const loadUsers = async () => {
try { try {
const token = localStorage.getItem('token');
const response = await axios.get(`${API_URL}/api/users`, { const response = await axios.get(`${API_URL}/api/users`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}); });
@@ -28,7 +28,7 @@ const UserManagement = ({ currentUser, addNotification }) => {
setLoading(false); setLoading(false);
} catch (error) { } catch (error) {
console.error('Ошибка загрузки пользователей:', error); console.error('Ошибка загрузки пользователей:', error);
addNotification('error', 'Ошибка загрузки пользователей'); notify('error', 'Ошибка загрузки', 'Не удалось загрузить пользователей');
setLoading(false); setLoading(false);
} }
}; };
@@ -47,12 +47,12 @@ const UserManagement = ({ currentUser, addNotification }) => {
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
addNotification('success', `Роль пользователя ${username} изменена на ${newRole}`); notify('success', 'Роль изменена', `Роль пользователя ${username} изменена на ${newRole}`);
loadUsers(); loadUsers();
setShowRoleModal(false); setShowRoleModal(false);
} catch (error) { } catch (error) {
console.error('Ошибка изменения роли:', error); console.error('Ошибка изменения роли:', error);
addNotification('error', error.response?.data?.detail || 'Ошибка изменения роли'); notify('error', 'Ошибка изменения', error.response?.data?.detail || 'Не удалось изменить роль');
} }
}; };
@@ -68,11 +68,11 @@ const UserManagement = ({ currentUser, addNotification }) => {
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
addNotification('success', `Пользователь ${username} заблокирован`); notify('success', 'Пользователь заблокирован', `${username} успешно заблокирован`);
loadUsers(); loadUsers();
} catch (error) { } catch (error) {
console.error('Ошибка блокировки:', error); console.error('Ошибка блокировки:', error);
addNotification('error', error.response?.data?.detail || 'Ошибка блокировки'); notify('error', 'Ошибка блокировки', error.response?.data?.detail || 'Не удалось заблокировать пользователя');
} }
}; };
@@ -86,11 +86,11 @@ const UserManagement = ({ currentUser, addNotification }) => {
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
addNotification('success', `Пользователь ${username} разблокирован`); notify('success', 'Пользователь разблокирован', `${username} успешно разблокирован`);
loadUsers(); loadUsers();
} catch (error) { } catch (error) {
console.error('Ошибка разблокировки:', error); console.error('Ошибка разблокировки:', error);
addNotification('error', error.response?.data?.detail || 'Ошибка разблокировки'); notify('error', 'Ошибка разблокировки', error.response?.data?.detail || 'Не удалось разблокировать пользователя');
} }
}; };
@@ -105,11 +105,11 @@ const UserManagement = ({ currentUser, addNotification }) => {
{ headers: { Authorization: `Bearer ${token}` } } { headers: { Authorization: `Bearer ${token}` } }
); );
addNotification('success', `Пользователь ${username} удалён`); notify('success', 'Пользователь удалён', `${username} успешно удалён`);
loadUsers(); loadUsers();
} catch (error) { } catch (error) {
console.error('Ошибка удаления:', error); console.error('Ошибка удаления:', error);
addNotification('error', error.response?.data?.detail || 'Ошибка удаления'); notify('error', 'Ошибка удаления', error.response?.data?.detail || 'Не удалось удалить пользователя');
} }
}; };

View File

@@ -2,29 +2,191 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
* { /* MCSManager Style Global Styles */
margin: 0; @layer base {
padding: 0; * {
box-sizing: border-box; @apply border-dark-700;
} }
body { body {
@apply bg-dark-900 text-gray-100 antialiased;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; }
-moz-osx-font-smoothing: grayscale;
/* Scrollbar styling */
::-webkit-scrollbar {
@apply w-2 h-2;
}
::-webkit-scrollbar-track {
@apply bg-dark-850;
}
::-webkit-scrollbar-thumb {
@apply bg-dark-600 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-dark-500;
}
} }
#root { @layer components {
width: 100%; /* Card styles */
height: 100vh; .card {
@apply bg-dark-800 rounded-lg border border-dark-700 shadow-lg;
}
.card-hover {
@apply card transition-all duration-200 hover:border-primary-500 hover:shadow-glow;
}
/* Button styles */
.btn {
@apply px-4 py-2 rounded-lg font-medium transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-dark-900;
}
.btn-primary {
@apply btn bg-primary-600 text-white hover:bg-primary-700
focus:ring-primary-500 shadow-md hover:shadow-glow;
}
.btn-secondary {
@apply btn bg-dark-700 text-gray-200 hover:bg-dark-600
focus:ring-dark-500 border border-dark-600;
}
.btn-success {
@apply btn bg-green-600 text-white hover:bg-green-700
focus:ring-green-500 shadow-md;
}
.btn-danger {
@apply btn bg-red-600 text-white hover:bg-red-700
focus:ring-red-500 shadow-md;
}
.btn-warning {
@apply btn bg-yellow-600 text-white hover:bg-yellow-700
focus:ring-yellow-500 shadow-md;
}
.btn-icon {
@apply p-2 rounded-lg transition-all duration-200
hover:bg-dark-700 focus:outline-none focus:ring-2
focus:ring-primary-500 focus:ring-offset-2 focus:ring-offset-dark-900;
}
/* Input styles */
.input {
@apply w-full px-4 py-2 bg-dark-850 border border-dark-700 rounded-lg
text-gray-100 placeholder-gray-500
focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent
transition-all duration-200;
}
.input:focus {
@apply shadow-glow;
}
/* Badge styles */
.badge {
@apply px-2 py-1 text-xs font-semibold rounded-full;
}
.badge-success {
@apply badge bg-green-600/20 text-green-400 border border-green-600/30;
}
.badge-danger {
@apply badge bg-red-600/20 text-red-400 border border-red-600/30;
}
.badge-warning {
@apply badge bg-yellow-600/20 text-yellow-400 border border-yellow-600/30;
}
.badge-info {
@apply badge bg-blue-600/20 text-blue-400 border border-blue-600/30;
}
/* Sidebar styles */
.sidebar-item {
@apply flex items-center gap-3 px-4 py-3 rounded-lg
text-gray-300 hover:text-white hover:bg-dark-700
transition-all duration-200 cursor-pointer;
}
.sidebar-item-active {
@apply sidebar-item bg-primary-600/20 text-primary-400
border-l-4 border-primary-500 hover:bg-primary-600/30;
}
/* Tab styles */
.tab {
@apply px-4 py-2 font-medium text-gray-400 hover:text-gray-200
border-b-2 border-transparent hover:border-dark-600
transition-all duration-200 cursor-pointer;
}
.tab-active {
@apply tab text-primary-400 border-primary-500 hover:border-primary-500;
}
/* Stats card */
.stat-card {
@apply card p-4 flex items-center gap-4;
}
.stat-icon {
@apply p-3 rounded-lg bg-primary-600/20 text-primary-400;
}
/* Server card */
.server-card {
@apply card-hover p-6 cursor-pointer;
}
.server-card-selected {
@apply server-card border-primary-500 bg-primary-600/10 shadow-glow;
}
/* Modal overlay */
.modal-overlay {
@apply fixed inset-0 bg-black/70 backdrop-blur-sm z-40
flex items-center justify-center p-4;
}
.modal-content {
@apply card p-6 max-w-5xl w-full max-h-[90vh] overflow-y-auto;
}
/* Console terminal */
.console-terminal {
@apply bg-dark-950 rounded-lg p-4 font-mono text-sm
text-green-400 overflow-auto border border-dark-700;
}
/* Loading spinner */
.spinner {
@apply animate-spin rounded-full border-2 border-gray-600
border-t-primary-500;
}
/* Tooltip */
.tooltip {
@apply absolute z-50 px-2 py-1 text-xs font-medium text-white
bg-dark-700 rounded shadow-lg pointer-events-none;
}
} }
/* Анимация для уведомлений */ /* Animations */
@keyframes slide-in-right { @keyframes slideIn {
from { from {
transform: translateX(100%); transform: translateX(-100%);
opacity: 0; opacity: 0;
} }
to { to {
@@ -33,6 +195,42 @@ body {
} }
} }
.animate-slide-in-right { @keyframes fadeIn {
animation: slide-in-right 0.3s ease-out; from {
opacity: 0;
}
to {
opacity: 1;
}
}
.animate-slide-in {
animation: slideIn 0.3s ease-out;
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
}
/* Glow effect for active elements */
.glow-effect {
position: relative;
}
.glow-effect::before {
content: '';
position: absolute;
inset: -2px;
border-radius: inherit;
padding: 2px;
background: linear-gradient(45deg, #0ea5e9, #3b82f6, #8b5cf6);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
opacity: 0;
transition: opacity 0.3s;
}
.glow-effect:hover::before {
opacity: 0.5;
} }

View File

@@ -5,7 +5,44 @@ export default {
"./src/**/*.{js,ts,jsx,tsx}", "./src/**/*.{js,ts,jsx,tsx}",
], ],
theme: { theme: {
extend: {}, extend: {
colors: {
// MCSManager style dark theme
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
},
dark: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
850: '#1a2332',
900: '#0f172a',
950: '#020617',
},
},
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
},
boxShadow: {
'glow': '0 0 20px rgba(14, 165, 233, 0.3)',
'glow-lg': '0 0 30px rgba(14, 165, 233, 0.4)',
},
},
}, },
plugins: [], plugins: [],
} }

2
key.py Normal file
View File

@@ -0,0 +1,2 @@
import secrets
print(secrets.token_urlsafe(32))

97
nginx/default.conf Normal file
View File

@@ -0,0 +1,97 @@
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Логирование
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Основные настройки
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
client_max_body_size 100M;
# Gzip сжатие
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/atom+xml image/svg+xml;
# Upstream для backend API
upstream mc_panel_api {
server mc-panel:8000;
}
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Статические файлы frontend
location / {
try_files $uri $uri/ /index.html;
# Кэширование статических файлов
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
# API запросы к backend
location /api/ {
proxy_pass http://mc_panel_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
# Таймауты
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket для консоли
location /ws/ {
proxy_pass http://mc_panel_api;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket таймауты
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
}
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# Безопасность
location ~ /\. {
deny all;
}
}
}

View File

View File

View File

@@ -0,0 +1,7 @@
{
"name": "sdfsfsf",
"displayName": "sdfsdf",
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui",
"owner": "Leuteg",
"daemonId": "local"
}

View File

@@ -0,0 +1,6 @@
{
"name": "sfsf",
"displayName": "sdf",
"startCommand": "java -Xmx2G -Xms1G -jar server.jar nogui",
"owner": "Leuteg"
}

38
users.json Normal file
View File

@@ -0,0 +1,38 @@
{
"Root": {
"username": "Root",
"password": "$2b$12$PAaomoUWn3Ip5ov.S/uYPeTIRiDMq7DbA57ahyYQnw3QHT2zuYMlG",
"role": "owner",
"servers": [
"dfgdfgdfg"
],
"permissions": {
"manage_users": true,
"manage_roles": true,
"manage_servers": true,
"manage_tickets": true,
"manage_files": true,
"delete_users": true,
"view_all_resources": true
},
"resource_access": {
"servers": [],
"tickets": [],
"files": []
}
},
"Leuteg": {
"username": "Leuteg",
"password": "$2b$12$EAK2ougYahmHPhdaP/vm5O9RMPgnvtCYb.8Z9HpSqNrVxComaZ68C",
"role": "owner",
"servers": [
"sfsf"
],
"permissions": {
"servers": true,
"tickets": true,
"users": false,
"files": true
}
}
}

View File

@@ -1,328 +0,0 @@
# ✅ Версия 1.1.0 готова!
**Дата:** 15 января 2026
**Статус:** ЗАВЕРШЕНО ✅
---
## 🎉 Что было добавлено
### 👑 Система прав и ролей
**Создано файлов:** 5
**Написано строк:** ~1,500
#### 1. OWNER_PERMISSIONS.md (~500 строк)
Полная документация системы прав:
- Обзор системы
- 5 ролей пользователей
- 7 типов прав
- 8 новых API эндпоинтов
- Примеры на Python, JavaScript, cURL
- Инструкции по миграции
- FAQ
#### 2. backend/migrate_users.py (~200 строк)
Скрипт автоматической миграции:
- Создание backup
- Назначение владельца
- Добавление прав
- Добавление доступа к ресурсам
- Показ результата
#### 3. MIGRATE_USERS.bat (~50 строк)
Bat файл для Windows:
- Проверка Python
- Проверка users.json
- Запуск миграции
- Показ результата
#### 4. CHANGELOG.md (~300 строк)
История изменений:
- Версия 1.1.0
- Версия 1.0.0
- Детальное описание
- Типы изменений
#### 5. VERSION_1.1.0.md (~400 строк)
Обзор релиза:
- Что нового
- Новые API
- Инструменты
- Примеры
- Миграция
---
## 📊 Статистика
### Новые возможности
**Роли:** 2 → 5
- Owner (новая)
- Admin
- Support (новая)
- User
- Banned (новая)
**Права:** 0 → 7
1. manage_users
2. manage_roles
3. manage_servers
4. manage_tickets
5. manage_files
6. delete_users
7. view_all_resources
**API эндпоинты:** 37 → 45 (+8)
- GET /api/users
- PUT /api/users/{id}/role
- PUT /api/users/{id}/permissions
- POST /api/users/{id}/access/servers
- DELETE /api/users/{id}/access/servers/{name}
- DELETE /api/users/{id}
- POST /api/users/{id}/ban
- POST /api/users/{id}/unban
---
## 🎯 Основные возможности
### Для владельца (Owner)
✅ Управление всеми пользователями
✅ Изменение ролей
✅ Управление правами
✅ Выдача/отзыв доступа к ресурсам
✅ Блокировка/разблокировка
✅ Удаление пользователей
✅ Просмотр всех ресурсов
### Для администратора (Admin)
✅ Управление пользователями
✅ Управление серверами
✅ Просмотр всех тикетов
✅ Блокировка пользователей
❌ Изменение ролей
❌ Удаление пользователей
### Для поддержки (Support)
✅ Просмотр всех тикетов
✅ Ответ на тикеты
✅ Изменение статуса тикетов
❌ Управление серверами
❌ Управление пользователями
### Для пользователя (User)
✅ Управление своими серверами
✅ Создание тикетов
✅ Управление своими файлами
❌ Просмотр чужих ресурсов
❌ Управление пользователями
---
## 🔄 Миграция
### Автоматическая
```bash
# Windows
MIGRATE_USERS.bat
# Linux/Mac
cd backend
python migrate_users.py
```
### Что происходит
1. ✅ Создаётся backup users.json
2. ✅ Первый пользователь → Owner
3. ✅ Admin остаются Admin
4. ✅ Остальные → User
5. ✅ Всем добавляются права
6. ✅ Добавляется доступ к ресурсам
---
## 📚 Документация
### Обновлено
- ✅ README.md - Добавлены ссылки на новые файлы
- ✅ Версия проекта: 1.0.0 → 1.1.0
### Создано
- ✅ OWNER_PERMISSIONS.md (~500 строк)
- ✅ CHANGELOG.md (~300 строк)
- ✅ VERSION_1.1.0.md (~400 строк)
-ВЕРСИЯ_1.1.0_ГОТОВА.md (этот файл)
**Итого новой документации:** ~1,200 строк
---
## 🚀 Как использовать
### 1. Обновление с v1.0.0
```bash
# Backup
BACKUP_DATA.bat
# Остановка
STOP_DOCKER.bat
# Обновление кода
git pull origin main
# Миграция
MIGRATE_USERS.bat
# Запуск
START_DOCKER.bat
```
### 2. Первый запуск v1.1.0
```bash
# Запуск
START_DOCKER.bat
# Регистрация
# Первый пользователь = Owner!
# Создание пользователей
# Через API или UI
```
### 3. Управление пользователями
```python
import requests
token = "owner_token"
base_url = "http://localhost:8000"
headers = {"Authorization": f"Bearer {token}"}
# Получить пользователей
users = requests.get(f"{base_url}/api/users", headers=headers)
# Изменить роль
requests.put(
f"{base_url}/api/users/2/role",
headers=headers,
json={"role": "admin"}
)
# Выдать доступ
requests.post(
f"{base_url}/api/users/2/access/servers",
headers=headers,
json={"server_name": "Survival"}
)
```
---
## 🔒 Безопасность
### Новые меры
- ✅ Проверка прав на всех эндпоинтах
- ✅ Логирование действий владельца
- ✅ Защита от удаления владельца
- ✅ Детальный контроль доступа
### Рекомендации
1. Регулярно проверяйте права
2. Используйте роль Support для поддержки
3. Блокируйте неактивных пользователей
4. Создавайте backup перед изменениями
---
## 📊 Сравнение версий
| Параметр | v1.0.0 | v1.1.0 |
|----------|--------|--------|
| Ролей | 2 | 5 |
| Прав | 0 | 7 |
| API | 37 | 45 |
| Файлов | 65+ | 70+ |
| Строк кода | ~9,300 | ~9,500 |
| Строк документации | ~6,000 | ~7,200 |
---
## 🎯 Что дальше?
### Планы на v1.2.0
- [ ] UI компонент управления пользователями
- [ ] Логи действий администраторов
- [ ] Экспорт/импорт пользователей
- [ ] Групповое управление правами
- [ ] История изменений прав
- [ ] Email уведомления
---
## ✅ Checklist
### Код
- [x] Скрипт миграции создан
- [x] Bat файл создан
- [x] Проверка прав добавлена
- [x] API эндпоинты работают
### Документация
- [x] OWNER_PERMISSIONS.md создан
- [x] CHANGELOG.md создан
- [x] VERSION_1.1.0.md создан
- [x] README.md обновлён
- [x] Версия обновлена
### Тестирование
- [x] Миграция протестирована
- [x] API эндпоинты протестированы
- [x] Права проверены
- [x] Backup работает
---
## 🏆 Итог
**Версия 1.1.0 полностью готова!**
### Добавлено:
- ✅ 5 ролей пользователей
- ✅ 7 типов прав
- ✅ 8 новых API эндпоинтов
- ✅ Инструменты миграции
- ✅ ~1,500 строк нового кода и документации
### Обновлено:
- ✅ README.md
- ✅ Версия проекта
- ✅ Структура пользователя
### Протестировано:
- ✅ Миграция работает
- ✅ API работает
- ✅ Права работают
---
**Версия:** 1.1.0
**Дата:** 15 января 2026
**Статус:** ГОТОВО К ИСПОЛЬЗОВАНИЮ ✅
**Полный контроль над панелью!** 👑🚀

View File

@@ -1,225 +0,0 @@
# ✅ Документация объединена!
## Что было сделано
### 📚 Создано 3 основных файла:
#### 1. **README.md** - Главная страница
Навигация по всей документации с кратким описанием проекта.
**Содержит:**
- Ссылки на всю документацию
- Быстрый старт
- Основные возможности
- Структура проекта
- Информация о поддержке
**Начните отсюда!** 👈
---
#### 2. **ДОКУМЕНТАЦИЯ.md** - Полная документация проекта
Вся документация проекта в одном файле (кроме API).
**Разделы:**
1. О проекте
2. Быстрый старт
3. Установка и настройка
4. Функциональность
5. Система уведомлений
6. Дизайн и темы
7. Файловый менеджер
8. Система тикетов
9. Личный кабинет
10. OpenID Connect
11. Роли пользователей
12. Безопасность
13. Troubleshooting
14. Дополнительная информация
15. Changelog
**Объем:** ~500 строк
---
#### 3. **API.md** - API документация
Вся API документация в одном файле.
**Разделы:**
1. Базовая информация
2. Быстрый старт
3. Аутентификация (3 эндпоинта)
4. Управление пользователями (4 эндпоинта)
5. Личный кабинет (4 эндпоинта)
6. Управление серверами (10 эндпоинтов)
7. Управление файлами (9 эндпоинтов)
8. Тикеты (5 эндпоинтов)
9. OpenID Connect (3 эндпоинта)
10. Коды ошибок
11. Примеры интеграции (Python, JavaScript, cURL)
12. Postman коллекция
**Всего эндпоинтов:** 37
**Объем:** ~300 строк
---
### 🗑️ Удалено 8 старых файлов:
- ❌ API_README.md
- ❌ API_QUICK_REFERENCE.md
- ❌ API_ДОКУМЕНТАЦИЯ.md
- ❌ API_DOCUMENTATION.md
- ❌ ДОКУМЕНТАЦИЯ_ГОТОВА.md
-СИСТЕМА_УВЕДОМЛЕНИЙ.md
-УВЕДОМЛЕНИЯ_ТИКЕТОВ.md
-ОБНОВЛЕНИЕ_УВЕДОМЛЕНИЙ.md
---
### 📦 Сохранено:
-**MC_Panel_API.postman_collection.json** - Postman коллекция
-Все остальные технические .md файлы (история разработки)
---
## 📊 Статистика
### Было:
- 📄 12+ разрозненных .md файлов
- 🔀 Дублирование информации
- 😕 Сложная навигация
### Стало:
- 📄 3 основных файла
- ✨ Вся информация структурирована
- 🎯 Простая навигация
- 📖 Легко найти нужное
---
## 🎯 Структура документации
```
MC Panel/
├── README.md # 👈 Начните здесь!
│ ├── Навигация
│ ├── Быстрый старт
│ └── Ссылки на документацию
├── ДОКУМЕНТАЦИЯ.md # Полная документация
│ ├── О проекте
│ ├── Установка
│ ├── Функциональность
│ ├── Система уведомлений
│ ├── Файловый менеджер
│ ├── Тикеты
│ ├── Личный кабинет
│ ├── OpenID Connect
│ ├── Роли
│ ├── Безопасность
│ └── Troubleshooting
├── API.md # API документация
│ ├── Все эндпоинты (37)
│ ├── Примеры запросов
│ ├── Коды ошибок
│ └── Интеграция (Python, JS, cURL)
└── MC_Panel_API.postman_collection.json # Postman коллекция
```
---
## 🚀 Как использовать
### Для пользователей:
1. Откройте **README.md** для обзора
2. Читайте **ДОКУМЕНТАЦИЯ.md** для изучения функций
3. Используйте **Troubleshooting** при проблемах
### Для разработчиков:
1. Откройте **README.md** для обзора
2. Читайте **API.md** для интеграции
3. Импортируйте **Postman коллекцию** для тестирования
### Для администраторов:
1. Читайте **ДОКУМЕНТАЦИЯ.md** → Установка и настройка
2. Изучите раздел **Безопасность**
3. Настройте **OpenID Connect**
---
## ✨ Преимущества новой структуры
### 1. Простота
- Всего 3 файла вместо 12+
- Легко найти нужную информацию
- Понятная навигация
### 2. Полнота
- Вся информация в одном месте
- Нет дублирования
- Актуальные данные
### 3. Удобство
- README с навигацией
- Разделение проект/API
- Быстрый поиск (Ctrl+F)
### 4. Поддержка
- Легко обновлять
- Легко добавлять новое
- Легко переводить
---
## 📝 Что включено
### ДОКУМЕНТАЦИЯ.md содержит:
- ✅ Быстрый старт
- ✅ Полная установка
-Все функции проекта
- ✅ Система уведомлений (полное описание)
- ✅ Дизайн и темы (6 тем)
- ✅ Файловый менеджер (все операции)
- ✅ Система тикетов (с уведомлениями)
- ✅ Личный кабинет
- ✅ OpenID Connect (ZITADEL)
- ✅ Роли пользователей (4 роли)
- ✅ Безопасность (рекомендации)
- ✅ Troubleshooting (решение проблем)
- ✅ Структура БД
- ✅ Горячие клавиши
- ✅ Советы и трюки
- ✅ Roadmap
- ✅ Changelog
### API.md содержит:
-Все 37 эндпоинтов
- ✅ Примеры запросов/ответов
- ✅ Коды ошибок
- ✅ Примеры интеграции:
- Python (класс MCPanelAPI)
- JavaScript (класс MCPanelAPI)
- cURL (готовые команды)
- ✅ Postman коллекция (описание)
- ✅ Безопасность API
- ✅ Лимиты и ограничения
- ✅ Changelog
---
## 🎉 Готово к использованию!
Вся документация объединена и структурирована.
**Начните с файла README.md** для навигации по документации.
---
**Дата создания:** 15 января 2026
**Версия:** 1.0.0
**Приятного использования MC Panel!** 🎮

View File

@@ -1,998 +0,0 @@
# MC Panel - Полная документация проекта
**Версия:** 1.0.0
**Дата:** 15 января 2026
---
## 📋 Содержание
1. [О проекте](#о-проекте)
2. [Быстрый старт](#быстрый-старт)
3. [Установка и настройка](#установка-и-настройка)
4. [Функциональность](#функциональность)
5. [Система уведомлений](#система-уведомлений)
6. [Дизайн и темы](#дизайн-и-темы)
7. [Файловый менеджер](#файловый-менеджер)
8. [Система тикетов](#система-тикетов)
9. [Личный кабинет](#личный-кабинет)
10. [OpenID Connect](#openid-connect)
11. [Роли пользователей](#роли-пользователей)
12. [Безопасность](#безопасность)
13. [Troubleshooting](#troubleshooting)
---
## О проекте
MC Panel - это современная веб-панель для управления Minecraft серверами с полным набором функций.
### Основные возможности
- 🖥️ **Управление серверами** - запуск, остановка, мониторинг
- 📁 **Файловый менеджер** - полное управление файлами сервера
- 💬 **Консоль** - отправка команд и просмотр логов в реальном времени
- 📊 **Статистика** - мониторинг CPU, RAM, диска
- 🎫 **Система тикетов** - поддержка пользователей
- 👥 **Управление пользователями** - роли и права доступа
- 🔐 **OpenID Connect** - интеграция с ZITADEL
- 🎨 **6 тем оформления** - включая современную темную тему
- 🔔 **Система уведомлений** - информирование о всех событиях
- 👤 **Личный кабинет** - управление профилем и статистика
### Технологии
**Backend:**
- FastAPI (Python)
- JWT аутентификация
- WebSocket для консоли
- Authlib для OpenID Connect
**Frontend:**
- React 18
- Tailwind CSS
- Axios для API запросов
- Lucide React для иконок
---
## Быстрый старт
### Шаг 1: Установка зависимостей
**Backend:**
```bash
cd backend
pip install -r requirements.txt
```
**Frontend:**
```bash
cd frontend
npm install
```
### Шаг 2: Настройка окружения
Создайте файл `.env` в корне проекта:
```env
# ZITADEL OpenID Connect
ZITADEL_ISSUER=https://your-instance.zitadel.cloud
ZITADEL_CLIENT_ID=your_client_id
ZITADEL_CLIENT_SECRET=your_client_secret
# URLs
BASE_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
```
### Шаг 3: Запуск
**Backend:**
```bash
cd backend
python main.py
```
Сервер запустится на `http://localhost:8000`
**Frontend:**
```bash
cd frontend
npm run dev
```
Интерфейс будет доступен на `http://localhost:3000`
### Шаг 4: Первый вход
1. Откройте `http://localhost:3000`
2. Зарегистрируйтесь (первый пользователь получит роль admin)
3. Создайте свой первый сервер
4. Загрузите server.jar в папку сервера
5. Запустите сервер!
**Учетные данные по умолчанию:**
- Логин: `Root`
- Пароль: `Admin`
---
## Установка и настройка
### Требования
- Python 3.8+
- Node.js 16+
- npm или yarn
- Git
### Полная установка
#### 1. Клонирование репозитория
```bash
git clone <repository-url>
cd mc-panel
```
#### 2. Установка Backend
```bash
cd backend
pip install -r requirements.txt
```
**Зависимости:**
- fastapi - веб-фреймворк
- uvicorn - ASGI сервер
- python-jose - JWT токены
- passlib - хеширование паролей
- python-multipart - загрузка файлов
- psutil - мониторинг системы
- authlib - OpenID Connect
- httpx - HTTP клиент
- python-dotenv - переменные окружения
#### 3. Установка Frontend
```bash
cd frontend
npm install
```
**Зависимости:**
- react - UI библиотека
- react-dom - рендеринг React
- axios - HTTP клиент
- lucide-react - иконки
- tailwindcss - CSS фреймворк
#### 4. Настройка ZITADEL (опционально)
Если хотите использовать OpenID Connect:
1. Создайте аккаунт на [ZITADEL](https://zitadel.com)
2. Создайте новое приложение (Application)
3. Выберите тип "Web"
4. Настройте Redirect URIs:
- `http://localhost:8000/api/auth/oidc/zitadel/callback`
5. Скопируйте Client ID и Client Secret
6. Добавьте в `.env` файл
#### 5. Структура проекта
```
mc-panel/
├── backend/
│ ├── main.py # Главный файл FastAPI
│ ├── oidc_config.py # Конфигурация OpenID Connect
│ ├── requirements.txt # Python зависимости
│ ├── users.json # База пользователей
│ ├── tickets.json # База тикетов
│ └── servers/ # Папка с серверами
├── frontend/
│ ├── src/
│ │ ├── App.jsx # Главный компонент
│ │ ├── components/ # React компоненты
│ │ ├── themes.js # Темы оформления
│ │ └── config.js # Конфигурация
│ ├── package.json # npm зависимости
│ └── vite.config.js # Конфигурация Vite
├── .env # Переменные окружения
└── ДОКУМЕНТАЦИЯ.md # Этот файл
```
---
## Функциональность
### Управление серверами
#### Создание сервера
1. Нажмите кнопку "+" в боковой панели
2. Заполните форму:
- **Имя папки** - только латиница, цифры, _ и -
- **Отображаемое имя** - любое название
- **Команда запуска** - команда для запуска сервера
3. Нажмите "Создать"
#### Запуск и остановка
- **Запустить** - зеленая кнопка "Запустить"
- **Остановить** - серая кнопка "Сброс"
- Статус отображается цветным индикатором
#### Консоль
- Просмотр логов в реальном времени
- Отправка команд серверу
- Цветная подсветка:
- 🟢 INFO - зеленый
- 🟡 WARN - желтый
- 🔴 ERROR - красный
- ⚪ Время - серый
#### Статистика
- **CPU** - использование процессора (%)
- **RAM** - использование памяти (МБ)
- **Disk** - размер файлов сервера (МБ)
- Обновление каждые 5 секунд
#### Настройки
- Изменение отображаемого имени
- Изменение команды запуска
- Удаление сервера (только админ)
---
## Система уведомлений
### Описание
Полноценная система уведомлений с автоматическим исчезновением через 5 секунд.
### Типы уведомлений
- 🟢 **Success** - успешные операции
- 🔴 **Error** - ошибки
- 🟡 **Warning** - предупреждения
- 🔵 **Info** - информационные сообщения
### Где используются
#### Управление серверами
- ✅ Сервер запущен
- Сервер остановлен
- ❌ Ошибки запуска/остановки
#### Файловый менеджер
- ✅ Файл/папка создан(а)
- ✅ Файл загружен/удален/сохранен
- ✅ Файл переименован/перемещен
- ❌ Ошибки операций
#### Тикеты
- ✅ Тикет создан
- ✅ Сообщение отправлено
- Новое сообщение (от других, каждые 3 сек)
- ✅ Статус изменён (действие)
- Статус изменён (просмотр)
- ❌ Ошибки
**Особенности тикетов:**
- Автообновление каждые 3 секунды
- Превью сообщений (50 символов)
- Не показываются для собственных действий
#### Личный кабинет
- ✅ Имя изменено
- ✅ Пароль изменён
- ❌ Ошибки
#### Создание сервера
- ✅ Сервер создан
- ❌ Ошибка создания
### Технические детали
**Компонент:** `frontend/src/components/NotificationSystem.jsx`
**Использование:**
```javascript
import { notify } from './components/NotificationSystem';
notify('success', 'Заголовок', 'Сообщение');
notify('error', 'Ошибка', 'Описание');
notify('warning', 'Внимание', 'Предупреждение');
notify('info', 'Информация', 'Сообщение');
```
**Анимация:** slide-in-right (0.3 сек)
---
## Дизайн и темы
### Доступные темы
1. **Modern (Современная)** - по умолчанию
- Цвета: #0f1115, #1a1d24, #23262e
- Акцент: зеленый
- Градиент: зеленый → изумрудный
2. **Dark (Тёмная)**
- Цвета: черный, темно-серый
- Акцент: синий
- Градиент: синий → фиолетовый
3. **Light (Светлая)**
- Цвета: белый, светло-серый
- Акцент: синий
- Градиент: синий → фиолетовый
4. **Purple (Фиолетовая)**
- Цвета: темный с фиолетовым оттенком
- Акцент: фиолетовый
- Градиент: фиолетовый → розовый
5. **Blue (Синяя)**
- Цвета: темный с синим оттенком
- Акцент: синий
- Градиент: голубой → синий
6. **Green (Зелёная)**
- Цвета: темный с зеленым оттенком
- Акцент: зеленый
- Градиент: изумрудный → зеленый
### Переключение темы
Кнопка в правом верхнем углу → выбор темы из списка
### Особенности дизайна
- Цветная консоль (INFO, WARN, ERROR)
- Кнопки с тенями и hover эффектами
- Плавные переходы и анимации
- Адаптивный дизайн
- Современные иконки (Lucide React)
---
## Файловый менеджер
### Возможности
#### Просмотр файлов
- Список файлов и папок
- 6 колонок: Имя, Тип, Размер, Изменение, Разрешение, Действия
- Поиск по названию
- Навигация по папкам
#### Создание
- **Файл** - кнопка "Новый" → "Создать файл"
- **Папка** - кнопка "Новый" → "Создать папку"
- Ввод имени и Enter
#### Загрузка и скачивание
- **Загрузить** - зеленая кнопка, выбор файла
- **Скачать** - иконка скачивания у файла
#### Редактирование
- **Просмотр** - иконка глаза
- **Редактирование** - иконка карандаша
- Сохранение изменений
#### Переименование
- Двойной клик по имени файла
- Ввод нового имени
- Enter для сохранения
#### Перемещение файлов
**Cut/Paste (Вырезать/Вставить):**
1. Выберите файлы чекбоксами
2. Нажмите "Вырезать" (оранжевая кнопка)
3. Перейдите в нужную папку
4. Нажмите "Вставить" (фиолетовая кнопка)
**Особенности:**
- Файлы подсвечиваются оранжевым
- Счетчик вырезанных файлов
- Кнопка "Отмена" для отмены операции
- Drag & Drop отключен
#### Удаление
- Иконка корзины
- Подтверждение удаления
- Удаление файлов и папок
#### Выбор файлов
- Чекбокс в заголовке - выбрать все
- Чекбоксы у файлов - выбор отдельных
- Кнопка "Обновить" - обновить список
### Интерфейс
- Поиск с иконкой
- Кнопки: Загрузить (зеленая), Обновить (серая), Новый (синяя)
- Кнопки перемещения: Вырезать (оранжевая), Вставить (фиолетовая), Отмена (серая)
- Таблица с hover эффектами
- "No data" при пустой папке
---
## Система тикетов
### Создание тикета
1. Кнопка "Тикеты" в шапке
2. Кнопка "+" для создания
3. Заполните:
- **Заголовок** - краткое описание
- **Описание** - подробности проблемы
4. Нажмите "Создать"
### Статусы тикетов
- 🟡 **На рассмотрении** (pending) - новый тикет
- 🔵 **В работе** (in_progress) - тикет взят в работу
- 🟢 **Закрыт** (closed) - проблема решена
### Работа с тикетами
#### Просмотр
- Список всех тикетов
- Фильтр по статусу (цветные индикаторы)
- Информация: автор, дата, статус
#### Чат
- Отправка сообщений
- Просмотр истории
- Автообновление каждые 3 секунды
- Уведомления о новых сообщениях
#### Изменение статуса (админ/поддержка)
- Кнопки статусов в шапке тикета
- Автоматическое системное сообщение
- Уведомление всем участникам
### Уведомления в тикетах
**При отправке сообщения:**
- ✅ "Сообщение отправлено"
**При получении сообщения:**
- "Новое сообщение: {автор}: {превью}..."
- Только от других пользователей
- Превью 50 символов
**При изменении статуса:**
- ✅ "Статус изменён: Тикет #X теперь: {статус}" (действие)
- "Статус изменён: Тикет #X: {статус}" (просмотр)
**При ошибках:**
- ❌ "Ошибка отправки" / "Ошибка изменения статуса"
### Права доступа
- **Пользователи** - видят только свои тикеты
- **Админы** - видят все тикеты, могут менять статус
- **Поддержка** - видят все тикеты, могут менять статус
---
## Личный кабинет
### Доступ
Кнопка "Личный кабинет" в правом верхнем углу
### Разделы
#### Обзор
- Имя пользователя и роль
- Статистика:
- Мои серверы (созданные)
- Доступные серверы (общие)
- Тикеты (всего, по статусам)
- Общее количество серверов
#### Безопасность
**Изменение имени пользователя:**
1. Введите новое имя (минимум 3 символа)
2. Введите текущий пароль
3. Нажмите "Изменить имя"
4. Получите новый токен
**Изменение пароля:**
1. Введите старый пароль
2. Введите новый пароль (минимум 6 символов)
3. Подтвердите новый пароль
4. Нажмите "Изменить пароль"
**Показ/скрытие паролей:**
- Иконка глаза для переключения видимости
### Просмотр чужих профилей (админ/поддержка)
Админы и техподдержка могут просматривать профили других пользователей:
- Статистика пользователя
- Список серверов
- Тикеты пользователя
- Индикатор "Просмотр профиля: {username}"
**Доступ:**
1. Раздел "Пользователи"
2. Кнопка "Профиль" у пользователя
---
## OpenID Connect
### Поддерживаемые провайдеры
- **ZITADEL** - основной провайдер
### Настройка ZITADEL
#### 1. Создание приложения
1. Зарегистрируйтесь на [ZITADEL](https://zitadel.com)
2. Создайте новый проект
3. Добавьте приложение (Application)
4. Выберите тип "Web"
5. Настройте Redirect URIs:
```
http://localhost:8000/api/auth/oidc/zitadel/callback
https://your-domain.com/api/auth/oidc/zitadel/callback
```
#### 2. Получение учетных данных
1. Скопируйте **Client ID**
2. Создайте и скопируйте **Client Secret**
3. Скопируйте **Issuer URL** (например: `https://your-instance.zitadel.cloud`)
#### 3. Настройка .env
```env
ZITADEL_ISSUER=https://your-instance.zitadel.cloud
ZITADEL_CLIENT_ID=your_client_id_here
ZITADEL_CLIENT_SECRET=your_client_secret_here
BASE_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
```
### Использование
#### Вход через ZITADEL
1. На странице входа нажмите кнопку "ZITADEL"
2. Авторизуйтесь на странице ZITADEL
3. Разрешите доступ приложению
4. Автоматическое перенаправление в панель
#### Создание пользователя
- При первом входе автоматически создается пользователь
- Имя пользователя берется из email (до @)
- Роль: `user` (обычный пользователь)
- Пароль не требуется (используется OIDC)
#### Связывание аккаунтов
- Каждый OIDC аккаунт уникален
- Повторный вход использует существующего пользователя
- ID хранится в формате: `zitadel:{sub}`
### Безопасность
- Токены обновляются автоматически
- Используется PKCE для защиты
- Все данные передаются через HTTPS (в production)
---
## Роли пользователей
### Admin (Администратор)
**Полный доступ ко всем функциям:**
- ✅ Все серверы (создание, удаление, управление)
- ✅ Все тикеты (просмотр, изменение статуса)
- ✅ Управление пользователями (роли, доступ, удаление)
- ✅ Просмотр всех профилей
- ✅ Изменение настроек серверов
**Получение роли:**
- Первый зарегистрированный пользователь
- Назначение другим админом
### User (Пользователь)
**Стандартные права:**
- ✅ Свои серверы (создание, управление)
- ✅ Серверы с предоставленным доступом
- ✅ Свои тикеты (создание, просмотр)
- ✅ Свой профиль
- ❌ Управление другими пользователями
- ❌ Удаление серверов
- ❌ Просмотр чужих тикетов
### Support (Техподдержка)
**Права поддержки:**
- ✅ Все тикеты (просмотр, ответы, изменение статуса)
- ✅ Просмотр профилей пользователей
- ✅ Свои серверы
- ❌ Управление пользователями
- ❌ Удаление серверов
- ❌ Изменение ролей
### Banned (Заблокирован)
**Нет доступа:**
- ❌ Вход в систему запрещен
- ❌ API запросы отклоняются
- ❌ Все функции недоступны
### Изменение ролей
**Только админы могут:**
1. Раздел "Пользователи"
2. Выбрать пользователя
3. Кнопка "Изменить роль"
4. Выбрать новую роль
5. Подтвердить
**Ограничения:**
- Нельзя изменить свою роль
- Нельзя удалить себя
---
## Безопасность
### JWT Токены
- **Алгоритм:** HS256
- **Срок действия:** 7 дней
- **Хранение:** localStorage (фронтенд)
- **Передача:** Bearer Token в заголовке Authorization
### Пароли
- **Хеширование:** bcrypt
- **Минимальная длина:** 6 символов
- **Проверка:** при каждом входе
- **Изменение:** требует старый пароль
### Файловая безопасность
- Все пути проверяются на выход за пределы папки сервера
- Запрещены операции с файлами вне `servers/`
- Проверка прав доступа к серверу
### API безопасность
- Все эндпоинты требуют авторизацию (кроме login/register)
- Проверка роли для админских функций
- Валидация входных данных
- Защита от SQL injection (используется JSON)
### Рекомендации для production
#### 1. Измените SECRET_KEY
В `backend/main.py`:
```python
SECRET_KEY = "your-very-long-random-secret-key-here"
```
#### 2. Используйте HTTPS
```env
BASE_URL=https://your-domain.com
FRONTEND_URL=https://your-domain.com
```
#### 3. Настройте CORS
В `backend/main.py`:
```python
app.add_middleware(
CORSMiddleware,
allow_origins=["https://your-domain.com"], # Конкретный домен
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
#### 4. Используйте базу данных
Замените `users.json` и `tickets.json` на PostgreSQL/MySQL
#### 5. Настройте firewall
- Ограничьте доступ к портам
- Разрешите только необходимые IP
#### 6. Регулярные обновления
```bash
pip install --upgrade -r requirements.txt
npm update
```
#### 7. Логирование
Включите подробное логирование для отслеживания действий
#### 8. Backup
Регулярно создавайте резервные копии:
- `users.json`
- `tickets.json`
- Папка `servers/`
---
## Troubleshooting
### Проблемы с запуском
#### Backend не запускается
```bash
# Проверьте Python версию
python --version # Должно быть 3.8+
# Переустановите зависимости
pip install --upgrade -r requirements.txt
# Проверьте порт 8000
netstat -ano | findstr :8000
```
#### Frontend не запускается
```bash
# Проверьте Node.js версию
node --version # Должно быть 16+
# Очистите кэш и переустановите
rm -rf node_modules package-lock.json
npm install
# Проверьте порт 3000
netstat -ano | findstr :3000
```
### Проблемы с аутентификацией
#### "Неверный токен"
- Токен истек (7 дней)
- Измененный SECRET_KEY
- **Решение:** Выйдите и войдите снова
#### "Требуется авторизация"
- Токен не передан
- Неверный формат токена
- **Решение:** Проверьте заголовок Authorization
#### OpenID Connect не работает
```bash
# Проверьте .env файл
cat .env
# Проверьте переменные
echo $ZITADEL_ISSUER
echo $ZITADEL_CLIENT_ID
# Проверьте Redirect URI в ZITADEL
# Должен быть: http://localhost:8000/api/auth/oidc/zitadel/callback
```
### Проблемы с серверами
#### Сервер не запускается
- Проверьте наличие `server.jar`
- Проверьте команду запуска
- Проверьте логи в консоли
- Проверьте права доступа к файлам
#### Консоль пустая
- Сервер еще не запущен
- WebSocket не подключен
- **Решение:** Перезапустите сервер
#### Статистика показывает 0
- Сервер остановлен
- Процесс завершился
- **Решение:** Запустите сервер
### Проблемы с файлами
#### "Файл не найден"
- Неверный путь
- Файл удален
- Нет прав доступа
- **Решение:** Проверьте путь и права
#### Не удается загрузить файл
- Файл слишком большой
- Нет места на диске
- **Решение:** Освободите место
#### Не удается переместить файл
- Файл открыт процессом
- Нет прав доступа
- **Решение:** Остановите сервер
### Проблемы с тикетами
#### Не приходят уведомления
- Проверьте интервал обновления (3 сек)
- Откройте консоль браузера (F12)
- Проверьте ошибки JavaScript
#### Сообщения не отправляются
- Проверьте подключение к интернету
- Проверьте токен авторизации
- Проверьте логи backend
### Проблемы с производительностью
#### Медленная работа
```bash
# Проверьте использование ресурсов
# Windows:
tasklist | findstr python
tasklist | findstr node
# Проверьте количество серверов
# Каждый сервер потребляет ресурсы
```
#### Высокое использование CPU
- Много запущенных серверов
- Частые обновления статистики
- **Решение:** Остановите неиспользуемые серверы
### Логи и отладка
#### Включить подробные логи (Backend)
В `backend/main.py`:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
```
#### Просмотр логов (Frontend)
Откройте консоль браузера (F12) → Console
#### Проверка API
```bash
# Проверьте доступность API
curl http://localhost:8000/api/auth/oidc/providers
# Проверьте токен
curl http://localhost:8000/api/auth/me \
-H "Authorization: Bearer YOUR_TOKEN"
```
### Получение помощи
1. **Проверьте документацию** - возможно, ответ уже есть
2. **Проверьте логи** - backend и frontend
3. **Создайте тикет** - опишите проблему подробно
4. **GitHub Issues** - для багов и предложений
---
## Дополнительная информация
### Структура базы данных
#### users.json
```json
{
"username": {
"username": "string",
"password": "hashed_password",
"role": "admin|user|support|banned",
"servers": ["server1", "server2"],
"oidc_id": "provider:sub",
"email": "user@example.com",
"name": "User Name",
"picture": "https://...",
"provider": "zitadel",
"created_at": "2026-01-15T10:00:00"
}
}
```
#### tickets.json
```json
{
"1": {
"id": "1",
"title": "Проблема",
"description": "Описание",
"author": "username",
"status": "pending|in_progress|closed",
"created_at": "2026-01-15T10:00:00",
"updated_at": "2026-01-15T11:00:00",
"messages": [
{
"author": "username",
"text": "Сообщение",
"timestamp": "2026-01-15T10:00:00"
}
]
}
}
```
#### panel_config.json (в папке сервера)
```json
{
"name": "server1",
"displayName": "My Server",
"startCommand": "java -Xmx2G -jar server.jar nogui",
"owner": "username"
}
```
### Горячие клавиши
- **Ctrl + K** - Поиск (в файловом менеджере)
- **Enter** - Отправить сообщение (в тикете)
- **Esc** - Закрыть модальное окно
- **F5** - Обновить страницу
### Советы и трюки
#### Быстрое создание сервера
1. Создайте сервер через панель
2. Загрузите `server.jar` через файловый менеджер
3. Создайте `eula.txt` с содержимым `eula=true`
4. Запустите сервер
#### Массовое перемещение файлов
1. Выберите все файлы (чекбокс в заголовке)
2. Нажмите "Вырезать"
3. Перейдите в папку назначения
4. Нажмите "Вставить"
#### Мониторинг нескольких серверов
Откройте панель в нескольких вкладках браузера для одновременного мониторинга
#### Быстрый доступ к консоли
Добавьте панель в закладки браузера для быстрого доступа
### Ограничения
- **Максимальный размер файла:** зависит от настроек сервера
- **Количество серверов:** не ограничено (зависит от ресурсов)
- **Количество пользователей:** не ограничено
- **Длина сообщения в тикете:** не ограничена
- **Срок хранения логов:** 1000 последних строк
### Roadmap (Планы развития)
- [ ] Поддержка нескольких OIDC провайдеров
- [ ] Расписание запуска/остановки серверов
- [ ] Автоматические бэкапы
- [ ] Графики статистики
- [ ] Плагин-менеджер
- [ ] Мобильное приложение
- [ ] Push-уведомления
- [ ] Двухфакторная аутентификация
- [ ] Темная тема для консоли
- [ ] Экспорт логов
---
## Changelog
### Версия 1.0.0 (15.01.2026)
- ✨ Первый релиз
- ✅ Управление серверами
- ✅ Файловый менеджер
- ✅ Система тикетов
- ✅ Личный кабинет
- ✅ OpenID Connect (ZITADEL)
- ✅ 6 тем оформления
- ✅ Система уведомлений
- ✅ Управление пользователями
- ✅ Роли и права доступа
- ✅ WebSocket консоль
- ✅ Мониторинг ресурсов
---
## Лицензия
MIT License - свободное использование
---
## Контакты и поддержка
- **Документация:** Этот файл
- **API Документация:** API.md
- **Тикеты:** Используйте систему тикетов в панели
- **GitHub:** [Ссылка на репозиторий]
---
**Спасибо за использование MC Panel!** 🎮
**Версия документации:** 1.0.0
**Дата обновления:** 15 января 2026