diff --git a/API.md b/API.md new file mode 100644 index 0000000..ad803d1 --- /dev/null +++ b/API.md @@ -0,0 +1,531 @@ +# 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 +``` + +### Заголовки запросов +``` +Content-Type: application/json +Authorization: Bearer +``` + +### Формат ответов +```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 ` + +**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!** 🚀 diff --git a/APPLY_FIXES.md b/APPLY_FIXES.md deleted file mode 100644 index 3e8990c..0000000 --- a/APPLY_FIXES.md +++ /dev/null @@ -1,25 +0,0 @@ -# Применение исправлений - -## Что исправлено - -### ✅ Фронтенд компоненты -Все компоненты обновлены для передачи токена: -- `Console.jsx` ✅ -- `Stats.jsx` ✅ -- `FileManager.jsx` ✅ -- `ServerSettings.jsx` ✅ -- `CreateServerModal.jsx` ✅ - -### ⚠️ Нужно обновить вручную - -#### 1. App.jsx (или App_final.jsx) - -Найдите строку: -```jsx -{user?.role === 'admin' && ( - - - ); -} -``` - -## Готово! 🎉 - -Теперь у вас современный интерфейс с системой тем! diff --git a/TICKETS_SYSTEM.md b/TICKETS_SYSTEM.md deleted file mode 100644 index 9b024ad..0000000 --- a/TICKETS_SYSTEM.md +++ /dev/null @@ -1,143 +0,0 @@ -# 🎫 Система тикетов - -## Что добавлено - -### ✅ Новые возможности - -1. **Система тикетов** - полноценная система поддержки с чатом -2. **Три статуса тикетов**: - - 🟡 **На рассмотрении** (pending) - новый тикет - - 🔵 **В работе** (in_progress) - тикет взят в работу - - 🟢 **Закрыт** (closed) - тикет решён - -3. **Новая роль "Тех. поддержка"** (support): - - Доступ ко всем тикетам - - Возможность менять статусы тикетов - - Возможность отвечать на тикеты - -4. **Кнопка "Тикеты"** в header рядом с кнопкой "Пользователи" - -### 📋 Возможности по ролям - -#### Обычные пользователи (user) -- ✅ Создавать тикеты -- ✅ Просматривать свои тикеты -- ✅ Отправлять сообщения в свои тикеты -- ❌ Менять статусы тикетов -- ❌ Видеть чужие тикеты - -#### Тех. поддержка (support) -- ✅ Просматривать все тикеты -- ✅ Отвечать на любые тикеты -- ✅ Менять статусы тикетов -- ✅ Закрывать тикеты -- ❌ Управлять пользователями -- ❌ Управлять серверами - -#### Администраторы (admin) -- ✅ Все возможности тех. поддержки -- ✅ Управление пользователями -- ✅ Управление серверами -- ✅ Назначение ролей - -## 🚀 Как использовать - -### Создание тикета -1. Нажмите кнопку "Тикеты" в header -2. Нажмите "Создать тикет" -3. Заполните тему и описание проблемы -4. Нажмите "Создать" - -### Работа с тикетом -1. Откройте список тикетов -2. Нажмите на нужный тикет -3. Пишите сообщения в чат -4. Тех. поддержка и админы могут менять статус тикета - -### Назначение роли "Тех. поддержка" -1. Войдите как администратор (none / none) -2. Нажмите кнопку "Пользователи" -3. Найдите нужного пользователя -4. В выпадающем списке выберите "Тех. поддержка" -5. Роль изменится автоматически - -## 📁 Новые файлы - -### Backend -- `backend/tickets.json` - хранилище тикетов (создаётся автоматически) -- Добавлены endpoints в `backend/main.py`: - - `GET /api/tickets` - список тикетов - - `POST /api/tickets/create` - создать тикет - - `GET /api/tickets/{id}` - получить тикет - - `POST /api/tickets/{id}/message` - добавить сообщение - - `PUT /api/tickets/{id}/status` - изменить статус - -### Frontend -- `frontend/src/components/Tickets.jsx` - список тикетов -- `frontend/src/components/TicketChat.jsx` - чат тикета -- `frontend/src/components/CreateTicketModal.jsx` - создание тикета - -## 🎨 Интерфейс - -### Список тикетов -- Карточки с информацией о тикете -- Цветные индикаторы статуса -- Количество сообщений -- Дата создания -- Автор тикета - -### Чат тикета -- Сообщения в реальном времени (обновление каждые 3 секунды) -- Системные сообщения о смене статуса -- Кнопки смены статуса (для тех. поддержки и админов) -- Отправка сообщений (если тикет не закрыт) - -## 🔧 Технические детали - -### Статусы тикетов -```javascript -pending // На рассмотрении (жёлтый) -in_progress // В работе (синий) -closed // Закрыт (зелёный) -``` - -### Роли пользователей -```javascript -user // Обычный пользователь -support // Тех. поддержка -admin // Администратор -``` - -### Структура тикета -```json -{ - "id": "1", - "title": "Проблема с сервером", - "description": "Описание проблемы", - "author": "username", - "status": "pending", - "created_at": "2024-01-14T12:00:00", - "updated_at": "2024-01-14T12:00:00", - "messages": [ - { - "author": "username", - "text": "Текст сообщения", - "timestamp": "2024-01-14T12:00:00" - } - ] -} -``` - -## ✅ Готово! - -Система тикетов полностью интегрирована в MC Panel. Пользователи могут создавать тикеты, а тех. поддержка и администраторы могут на них отвечать и управлять статусами. - -### Учётные данные по умолчанию -- **Логин**: Sofa12345 -- **Пароль**: arkonsad123 -- **Роль**: admin - -Для создания пользователя тех. поддержки: -1. Зарегистрируйте нового пользователя -2. Войдите как админ -3. Назначьте ему роль "Тех. поддержка" diff --git a/VIEW_USER_PROFILES.md b/VIEW_USER_PROFILES.md deleted file mode 100644 index 0ff7532..0000000 --- a/VIEW_USER_PROFILES.md +++ /dev/null @@ -1,187 +0,0 @@ -# 👁️ Просмотр профилей пользователей - -## Что добавлено - -### Возможность просмотра профилей для админов и тех. поддержки -Администраторы и сотрудники технической поддержки теперь могут просматривать личные кабинеты других пользователей, нажав на их логин в списке пользователей. - -## 🎯 Как использовать - -### Просмотр профиля пользователя -1. Войдите как администратор или тех. поддержка -2. Нажмите кнопку "Пользователи" в header -3. Найдите нужного пользователя в списке -4. **Нажмите на логин пользователя** (он теперь кликабельный и подсвечивается при наведении) -5. Откроется личный кабинет этого пользователя - -### Что можно увидеть -- ✅ Имя пользователя -- ✅ Роль пользователя -- ✅ Статистику по серверам (всего, мои, доступные) -- ✅ Список серверов пользователя -- ✅ Статистику по тикетам (всего, по статусам) - -### Что нельзя сделать -- ❌ Изменить имя пользователя (вкладка скрыта) -- ❌ Изменить пароль пользователя (вкладка скрыта) -- ❌ Редактировать профиль другого пользователя - -## 🎨 Визуальные изменения - -### В списке пользователей (Users.jsx) -- **Логин пользователя** теперь кликабельный -- При наведении логин подсвечивается синим цветом -- Курсор меняется на pointer (указатель) -- Подсказка "Просмотреть профиль" при наведении - -### В личном кабинете (Profile.jsx) -- **Заголовок**: "Профиль пользователя: [username]" (вместо "Личный кабинет") -- **Подзаголовок**: "Просмотр профиля другого пользователя" -- **Вкладки**: скрыты вкладки "Имя пользователя" и "Пароль" -- **Только вкладка "Обзор"**: показывается статистика пользователя - -## 📋 Технические детали - -### Backend (main.py) - -#### Новый endpoint -```python -@app.get("/api/profile/stats/{username}") -async def get_user_profile_stats(username: str, user: dict = Depends(get_current_user)): - """Получить статистику профиля другого пользователя""" - # Проверка прав доступа - if user["role"] not in ["admin", "support"]: - raise HTTPException(403, "Недостаточно прав") - - # Возвращает статистику указанного пользователя -``` - -#### Проверка прав -- Только администраторы и тех. поддержка могут просматривать чужие профили -- Обычные пользователи получат ошибку 403 - -### Frontend - -#### App.jsx -```javascript -const [viewingUsername, setViewingUsername] = useState(null); - -const handleViewProfile = (username) => { - setViewingUsername(username); - setShowProfile(true); - setShowUsers(false); -}; -``` - -#### Users.jsx -```javascript - -``` - -#### Profile.jsx -```javascript -const isViewingOther = viewingUsername && viewingUsername !== user?.username; - -const loadStats = async () => { - const endpoint = isViewingOther - ? `${API_URL}/api/profile/stats/${viewingUsername}` - : `${API_URL}/api/profile/stats`; - // ... -}; -``` - -## 🔐 Безопасность - -### Проверка прав на уровне API -- Endpoint `/api/profile/stats/{username}` проверяет роль пользователя -- Только `admin` и `support` могут получить доступ -- Обычные пользователи получат ошибку 403 - -### Защита на уровне UI -- Вкладки изменения имени и пароля скрыты при просмотре чужого профиля -- Невозможно редактировать данные другого пользователя -- Только просмотр статистики - -## 📊 Доступные роли - -### Кто может просматривать чужие профили -1. **Администратор** (admin) - ✅ Может просматривать все профили -2. **Тех. поддержка** (support) - ✅ Может просматривать все профили -3. **Пользователь** (user) - ❌ Не может просматривать чужие профили -4. **Забанен** (banned) - ❌ Не имеет доступа к панели - -## ✅ Примеры использования - -### Сценарий 1: Проверка активности пользователя -1. Админ хочет проверить, сколько серверов у пользователя -2. Открывает "Пользователи" -3. Нажимает на логин пользователя -4. Видит статистику: 3 сервера, 5 тикетов - -### Сценарий 2: Помощь пользователю -1. Тех. поддержка получила тикет от пользователя -2. Хочет посмотреть его серверы для диагностики -3. Открывает "Пользователи" -4. Нажимает на логин пользователя -5. Видит список серверов и их названия - -### Сценарий 3: Модерация -1. Админ хочет проверить активность пользователя перед баном -2. Открывает профиль пользователя -3. Видит статистику по тикетам и серверам -4. Принимает решение о блокировке - -## 🎯 Возврат к списку пользователей - -### Из профиля пользователя -1. Нажмите кнопку "Серверы" в header -2. Вы вернётесь к главной странице -3. Снова откройте "Пользователи" для просмотра других профилей - -### Или откройте свой профиль -1. Нажмите кнопку "Личный кабинет" в header -2. Откроется ваш собственный профиль -3. Будут доступны все вкладки (Обзор, Имя пользователя, Пароль) - -## ⚠️ Важные замечания - -### Ограничения -- Нельзя редактировать чужие профили -- Нельзя изменить имя или пароль другого пользователя -- Только просмотр статистики - -### Рекомендации -- Используйте эту функцию для помощи пользователям -- Не злоупотребляйте просмотром чужих профилей -- Соблюдайте конфиденциальность данных пользователей - -## ✅ Готово! - -Функция просмотра профилей пользователей полностью интегрирована в MC Panel. Администраторы и тех. поддержка могут легко просматривать информацию о пользователях для помощи и модерации. - -### Тестирование - -1. **Войдите как администратор** - - Логин: none - - Пароль: none - -2. **Создайте тестового пользователя** - - Зарегистрируйте нового пользователя - - Создайте несколько серверов от его имени - -3. **Просмотрите его профиль** - - Откройте "Пользователи" - - Нажмите на логин тестового пользователя - - Увидите его статистику - -4. **Вернитесь к своему профилю** - - Нажмите "Личный кабинет" - - Откроется ваш профиль со всеми вкладками - -**Удобного использования! 👁️** diff --git a/ZITADEL_QUICK_START.md b/ZITADEL_QUICK_START.md deleted file mode 100644 index 96b8239..0000000 --- a/ZITADEL_QUICK_START.md +++ /dev/null @@ -1,76 +0,0 @@ -# 🚀 Быстрый старт с ZITADEL - -## Что нужно сделать - -### 1️⃣ Создать приложение в ZITADEL - -1. Зайдите на [zitadel.cloud](https://zitadel.cloud) или используйте свой инстанс -2. Создайте новый проект или выберите существующий -3. Нажмите **"New Application"** -4. Выберите **"Web Application"** -5. Выберите **"Code (with PKCE)"** -6. Добавьте Redirect URI: `http://localhost:8000/api/auth/oidc/zitadel/callback` -7. Сохраните **Client ID** и **Client Secret** - -### 2️⃣ Настроить .env файл - -Откройте `backend/.env` и добавьте: - -```bash -# ZITADEL Configuration -ZITADEL_ISSUER=https://your-instance.zitadel.cloud -ZITADEL_CLIENT_ID=123456789012345678@your-project -ZITADEL_CLIENT_SECRET=your-secret-key-here - -# URLs -BASE_URL=http://localhost:8000 -FRONTEND_URL=http://localhost:3000 -``` - -### 3️⃣ Запустить приложение - -```bash -# Backend -cd backend -python main.py - -# Frontend (в другом терминале) -cd frontend -npm run dev -``` - -### 4️⃣ Проверить - -1. Откройте http://localhost:3000 -2. Увидите кнопку **"Войти через ZITADEL"** 🔐 -3. Нажмите и войдите через ZITADEL -4. Готово! ✅ - -## Что происходит? - -1. **Пользователь нажимает кнопку** → Перенаправление на ZITADEL -2. **Вход в ZITADEL** → Пользователь вводит логин/пароль -3. **Callback** → ZITADEL возвращает код авторизации -4. **Обмен кода на токен** → Backend получает данные пользователя -5. **Создание пользователя** → Автоматическое создание в системе -6. **JWT токен** → Пользователь получает токен для доступа -7. **Автоматический вход** → Перенаправление в панель - -## Проблемы? - -### Кнопка ZITADEL не появляется -- Проверьте `.env` файл -- Убедитесь что `ZITADEL_CLIENT_ID` и `ZITADEL_ISSUER` заполнены -- Перезапустите backend - -### Ошибка "Invalid redirect_uri" -- Проверьте Redirect URI в настройках ZITADEL -- Должен быть: `http://localhost:8000/api/auth/oidc/zitadel/callback` - -### Ошибка "Invalid client" -- Проверьте `ZITADEL_CLIENT_ID` и `ZITADEL_CLIENT_SECRET` -- Убедитесь что приложение активно в ZITADEL - -## Готово! 🎉 - -Теперь пользователи могут входить через ZITADEL! diff --git a/backend/main.py b/backend/main.py index 66307ef..246c59d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -73,8 +73,8 @@ IS_WINDOWS = sys.platform == 'win32' def init_users(): if not USERS_FILE.exists(): admin_user = { - "username": "Sofa12345", - "password": pwd_context.hash("arkonsad123"), + "username": "Root", + "password": pwd_context.hash("Admin"), "role": "admin", "servers": [] } @@ -1005,23 +1005,88 @@ async def download_file(server_name: str, path: str, user: dict = Depends(get_cu @app.post("/api/servers/{server_name}/files/upload") async def upload_file(server_name: str, path: str, file: UploadFile = File(...), user: dict = Depends(get_current_user)): + print(f"Upload request: server={server_name}, path='{path}', filename='{file.filename}'") + if not check_server_access(user, server_name): raise HTTPException(403, "Нет доступа к этому серверу") server_path = SERVERS_DIR / server_name target_path = server_path / path / file.filename + print(f"Target path: {target_path}") + print(f"Server path: {server_path}") + print(f"Path starts with server_path: {str(target_path).startswith(str(server_path))}") + if not str(target_path).startswith(str(server_path)): raise HTTPException(400, "Недопустимый путь") - target_path.parent.mkdir(parents=True, exist_ok=True) + try: + target_path.parent.mkdir(parents=True, exist_ok=True) + print(f"Created directory: {target_path.parent}") + except Exception as e: + print(f"Error creating directory: {e}") + raise HTTPException(500, f"Ошибка создания директории: {str(e)}") - with open(target_path, "wb") as f: - content = await file.read() - f.write(content) + try: + with open(target_path, "wb") as f: + content = await file.read() + f.write(content) + print(f"File written successfully: {target_path}") + except Exception as e: + print(f"Error writing file: {e}") + raise HTTPException(500, f"Ошибка записи файла: {str(e)}") return {"message": "Файл загружен"} +@app.post("/api/servers/{server_name}/files/create") +async def create_file_or_folder(server_name: str, data: dict, user: dict = Depends(get_current_user)): + """Создать новый файл или папку""" + if not check_server_access(user, server_name): + raise HTTPException(403, "Нет доступа к этому серверу") + + item_type = data.get("type") # "file" or "folder" + name = data.get("name", "").strip() + path = data.get("path", "") # Текущая папка + + if not name: + raise HTTPException(400, "Имя не может быть пустым") + + if item_type not in ["file", "folder"]: + raise HTTPException(400, "Тип должен быть 'file' или 'folder'") + + server_path = SERVERS_DIR / server_name + + # Формируем полный путь + if path: + full_path = server_path / path / name + else: + full_path = server_path / name + + print(f"Creating {item_type}: {full_path}") + + # Проверка безопасности + if not str(full_path).startswith(str(server_path)): + raise HTTPException(400, "Недопустимый путь") + + try: + if item_type == "folder": + # Создаем папку + full_path.mkdir(parents=True, exist_ok=True) + # Создаем .gitkeep чтобы папка не была пустой + gitkeep = full_path / ".gitkeep" + gitkeep.touch() + print(f"Folder created: {full_path}") + else: + # Создаем файл + full_path.parent.mkdir(parents=True, exist_ok=True) + full_path.touch() + print(f"File created: {full_path}") + + return {"message": f"{'Папка' if item_type == 'folder' else 'Файл'} создан(а)", "path": str(full_path)} + except Exception as e: + print(f"Error creating {item_type}: {e}") + raise HTTPException(500, f"Ошибка создания: {str(e)}") + @app.delete("/api/servers/{server_name}/files") async def delete_file(server_name: str, path: str, user: dict = Depends(get_current_user)): if not check_server_access(user, server_name): @@ -1098,6 +1163,82 @@ async def rename_file(server_name: str, old_path: str, new_name: str, user: dict old_file_path.rename(new_file_path) return {"message": "Файл переименован"} +@app.post("/api/servers/{server_name}/files/move") +async def move_file(server_name: str, data: dict, user: dict = Depends(get_current_user)): + """Переместить файл или папку""" + if not check_server_access(user, server_name): + raise HTTPException(403, "Нет доступа к этому серверу") + + source_path = data.get("source", "").strip() + destination_path = data.get("destination", "").strip() + + if not source_path: + raise HTTPException(400, "Не указан исходный путь") + + server_path = SERVERS_DIR / server_name + source_full = server_path / source_path + + # Формируем путь назначения + if destination_path: + # Извлекаем имя файла из source_path + file_name = source_full.name + dest_full = server_path / destination_path / file_name + else: + # Перемещение в корень + file_name = source_full.name + dest_full = server_path / file_name + + print(f"Moving: {source_full} -> {dest_full}") + + # Проверки безопасности + if not source_full.exists(): + raise HTTPException(404, "Исходный файл не найден") + + if not str(source_full).startswith(str(server_path)): + raise HTTPException(400, "Недопустимый исходный путь") + + if not str(dest_full).startswith(str(server_path)): + raise HTTPException(400, "Недопустимый путь назначения") + + if dest_full.exists(): + raise HTTPException(400, "Файл с таким именем уже существует в папке назначения") + + try: + # Создаем папку назначения если не существует + dest_full.parent.mkdir(parents=True, exist_ok=True) + + # Перемещаем файл/папку + import shutil + shutil.move(str(source_full), str(dest_full)) + + print(f"Moved successfully: {dest_full}") + return {"message": "Файл перемещен", "new_path": str(dest_full)} + except Exception as e: + print(f"Error moving file: {e}") + raise HTTPException(500, f"Ошибка перемещения: {str(e)}") + +@app.put("/api/servers/{server_name}/files/rename") +async def rename_file(server_name: str, old_path: str, new_name: str, user: dict = Depends(get_current_user)): + if not check_server_access(user, server_name): + raise HTTPException(403, "Нет доступа к этому серверу") + + server_path = SERVERS_DIR / server_name + old_file_path = server_path / old_path + + if not old_file_path.exists() or not str(old_file_path).startswith(str(server_path)): + raise HTTPException(404, "Файл не найден") + + new_file_path = old_file_path.parent / new_name + + if new_file_path.exists(): + raise HTTPException(400, "Файл с таким именем уже существует") + + if not str(new_file_path).startswith(str(server_path)): + raise HTTPException(400, "Недопустимое имя файла") + + old_file_path.rename(new_file_path) + return {"message": "Файл переименован"} + # API для тикетов @app.get("/api/tickets") async def get_tickets(user: dict = Depends(get_current_user)): diff --git a/backend/tickets.json b/backend/tickets.json index 2af8375..9e26dfe 100644 --- a/backend/tickets.json +++ b/backend/tickets.json @@ -1,129 +1 @@ -{ - "1": { - "id": "1", - "title": "Пошёл нахуй", - "description": "Свин", - "author": "arkonsad", - "status": "closed", - "created_at": "2026-01-14T15:20:26.344010", - "updated_at": "2026-01-14T15:22:02.654579", - "messages": [ - { - "author": "arkonsad", - "text": "Свин", - "timestamp": "2026-01-14T15:20:26.344010" - }, - { - "author": "Sofa12345", - "text": "Ты че", - "timestamp": "2026-01-14T15:21:19.943424" - }, - { - "author": "Sofa12345", - "text": "ахуел", - "timestamp": "2026-01-14T15:21:24.251787" - }, - { - "author": "arkonsad", - "text": "покушай говна", - "timestamp": "2026-01-14T15:21:46.676746" - }, - { - "author": "system", - "text": "Статус изменён на: В работе", - "timestamp": "2026-01-14T15:21:48.504108" - }, - { - "author": "Sofa12345", - "text": "тварина ты ебаная", - "timestamp": "2026-01-14T15:21:58.245227" - }, - { - "author": "system", - "text": "Статус изменён на: Закрыт", - "timestamp": "2026-01-14T15:22:02.654579" - } - ] - }, - "2": { - "id": "2", - "title": "Разраб даун", - "description": "помогите разраб минды даун, а киро вообще маньяк на коммиты в гитею", - "author": "MihailPrud", - "status": "closed", - "created_at": "2026-01-15T03:25:33.660528", - "updated_at": "2026-01-15T03:27:41.117949", - "messages": [ - { - "author": "MihailPrud", - "text": "помогите разраб минды даун, а киро вообще маньяк на коммиты в гитею", - "timestamp": "2026-01-15T03:25:33.660528" - }, - { - "author": "system", - "text": "Статус изменён на: В работе", - "timestamp": "2026-01-15T03:25:56.445796" - }, - { - "author": "Sofa12345", - "text": "Дааааа, туда этого бота", - "timestamp": "2026-01-15T03:25:58.592839" - }, - { - "author": "MihailPrud", - "text": "памагете", - "timestamp": "2026-01-15T03:26:20.740325" - }, - { - "author": "Sofa12345", - "text": "чим", - "timestamp": "2026-01-15T03:26:29.038071" - }, - { - "author": "MihailPrud", - "text": "у миня -30 и минет в школу надоть", - "timestamp": "2026-01-15T03:26:37.692369" - }, - { - "author": "Sofa12345", - "text": "пиздец нахуй блять", - "timestamp": "2026-01-15T03:26:48.846565" - }, - { - "author": "MihailPrud", - "text": "согласен", - "timestamp": "2026-01-15T03:26:56.324587" - }, - { - "author": "Sofa12345", - "text": "Nahyi eto school nyxna", - "timestamp": "2026-01-15T03:27:15.968192" - }, - { - "author": "Sofa12345", - "text": "pizdets", - "timestamp": "2026-01-15T03:27:21.810953" - }, - { - "author": "MihailPrud", - "text": "не нужна", - "timestamp": "2026-01-15T03:27:24.548623" - }, - { - "author": "MihailPrud", - "text": "но ходить надоть", - "timestamp": "2026-01-15T03:27:31.625634" - }, - { - "author": "system", - "text": "Статус изменён на: Закрыт", - "timestamp": "2026-01-15T03:27:38.480740" - }, - { - "author": "MihailPrud", - "text": "для баланса вселеннной", - "timestamp": "2026-01-15T03:27:41.117949" - } - ] - } -} \ No newline at end of file +{} \ No newline at end of file diff --git a/backend/users.json b/backend/users.json index 1e5152d..e005066 100644 --- a/backend/users.json +++ b/backend/users.json @@ -11,15 +11,10 @@ "arkonsad": { "username": "arkonsad", "password": "$2b$12$z.AYkfa/MlTYFd9rLNfBmu9JHOFKUe8YdddnqCmRqAxc7vGQeo392", - "role": "banned", + "role": "user", "servers": [ - "123" + "123", + "sdfsdf" ] - }, - "Sofa12345": { - "username": "Sofa12345", - "password": "$2b$12$Fph20p2mwgOAqoT77wSA3.n1S7NiHLa28aiNOwWcz3PfNhgC5pp5.", - "role": "admin", - "servers": [] } } \ No newline at end of file diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4f57048..d1f7beb 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,6 +11,7 @@ import Profile from './components/Profile'; import Auth from './components/Auth'; import ErrorBoundary from './components/ErrorBoundary'; import ThemeSelector from './components/ThemeSelector'; +import NotificationSystem, { notify } from './components/NotificationSystem'; import axios from 'axios'; import { API_URL } from './config'; import { getTheme } from './themes'; @@ -27,7 +28,7 @@ function App() { const [showProfile, setShowProfile] = useState(false); const [viewingUsername, setViewingUsername] = useState(null); const [connectionError, setConnectionError] = useState(false); - const [theme, setTheme] = useState(localStorage.getItem('theme') || 'dark'); + const [theme, setTheme] = useState(localStorage.getItem('theme') || 'modern'); const [sidebarOpen, setSidebarOpen] = useState(true); const currentTheme = getTheme(theme); @@ -149,11 +150,13 @@ function App() { { headers: { Authorization: `Bearer ${token}` } } ); console.log('Сервер запущен:', response.data); + notify('success', 'Сервер запущен', `Сервер "${serverName}" успешно запущен`); setTimeout(() => { loadServers(); }, 1000); } catch (error) { console.error('Ошибка запуска сервера:', error); + notify('error', 'Ошибка запуска', error.response?.data?.detail || 'Не удалось запустить сервер'); alert(error.response?.data?.detail || 'Ошибка запуска сервера'); } }; @@ -166,11 +169,13 @@ function App() { { 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 || 'Ошибка остановки сервера'); } }; @@ -334,6 +339,7 @@ function App() { return (
+ {/* Header */}
@@ -486,6 +492,42 @@ function App() {
{selectedServer ? ( <> + {/* Server Header with Controls */} +
+
+ + + {servers.find(s => s.name === selectedServer)?.displayName || selectedServer} + + s.name === selectedServer)?.status === 'running' + ? 'bg-green-600 text-white' + : 'bg-gray-600 text-white' + }`}> + {servers.find(s => s.name === selectedServer)?.status === 'running' ? 'Запущен' : 'Остановлен'} + +
+
+ {servers.find(s => s.name === selectedServer)?.status === 'stopped' ? ( + + ) : ( + + )} +
+
+ {/* Tabs */}
{[ diff --git a/frontend/src/components/Auth.jsx b/frontend/src/components/Auth.jsx index dff8eb1..11a283b 100644 --- a/frontend/src/components/Auth.jsx +++ b/frontend/src/components/Auth.jsx @@ -11,7 +11,7 @@ export default function Auth({ onLogin }) { const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); - const [theme] = useState(localStorage.getItem('theme') || 'dark'); + const [theme] = useState(localStorage.getItem('theme') || 'modern'); const [oidcProviders, setOidcProviders] = useState({}); const currentTheme = getTheme(theme); @@ -182,7 +182,7 @@ export default function Auth({ onLogin }) { {isLogin && (

Учётные данные по умолчанию:

-

Sofa12345 / arkonsad123

+

none / none

)}
diff --git a/frontend/src/components/Console.jsx b/frontend/src/components/Console.jsx index c234197..2f279c2 100644 --- a/frontend/src/components/Console.jsx +++ b/frontend/src/components/Console.jsx @@ -54,32 +54,63 @@ export default function Console({ serverName, token, theme }) { } }; + // Функция для раскраски логов + const colorizeLog = (log) => { + // INFO - зеленый + if (log.includes('[INFO]') || log.includes('Done (')) { + return {log}; + } + // WARN - желтый + if (log.includes('[WARN]') || log.includes('WARNING')) { + return {log}; + } + // ERROR - красный + if (log.includes('[ERROR]') || log.includes('Exception')) { + return {log}; + } + // Время - серый + if (log.match(/^\[\d{2}:\d{2}:\d{2}\]/)) { + const time = log.match(/^\[\d{2}:\d{2}:\d{2}\]/)[0]; + const rest = log.substring(time.length); + return ( + <> + {time} + {rest} + + ); + } + // Обычный текст + return {log}; + }; + return (
-
+ {/* Консоль */} +
{logs.length === 0 ? (
Консоль пуста. Запустите сервер для просмотра логов.
) : ( logs.map((log, index) => ( -
- {log} +
+ {colorizeLog(log)}
)) )}
+ {/* Поле ввода команды */}
setCommand(e.target.value)} - placeholder="Введите команду..." - className={`flex-1 ${theme.input} ${theme.border} border rounded-xl px-4 py-2 ${theme.text} focus:outline-none focus:ring-2 focus:ring-blue-500 transition`} + placeholder="Введите команду и нажмите Enter для отправки, используйте стрелки для навигации между предыдущими командами" + 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`} />
-
+