From c7fdaa566022c6f2676664bdfeacc80a12c91aef Mon Sep 17 00:00:00 2001 From: mirivlad Date: Sat, 14 Feb 2026 15:08:14 +0000 Subject: [PATCH] Initial commit - mon.mirv.top monitoring system --- AGENTS.md | 124 ++ INSTALL.md | 149 ++ TECHNICAL_SPECIFICATION.md | 183 +++ composer.json | 26 + composer.lock | 1206 +++++++++++++++++ config/DatabaseConfig.php | 44 + public/debug-login.php | 57 + public/index.php | 207 +++ public/index.php.broken | 202 +++ public/login-direct.php | 46 + public/session_check.php | 12 + public/session_test.php | 23 + public/set_session.php | 19 + schema.sql | 107 ++ src/Controllers/AdminController.php | 53 + src/Controllers/AgentController.php | 424 ++++++ src/Controllers/AlertController.php | 60 + src/Controllers/Api/MetricsController.php | 229 ++++ src/Controllers/DashboardController.php | 37 + src/Controllers/GroupController.php | 189 +++ src/Controllers/ServerController.php | 215 +++ src/Controllers/ServerDetailController.php | 197 +++ .../ServerDetailController.php.broken | 198 +++ src/Middlewares/AuthMiddleware.php | 27 + src/Middlewares/CsrfMiddleware.php | 44 + src/Middlewares/SessionMiddleware.php | 35 + src/Models/Alert.php | 46 + src/Models/Group.php | 62 + src/Models/Model.php | 27 + src/Models/Server.php | 59 + src/Models/User.php | 42 + src/Utils/EncryptionHelper.php | 37 + templates/admin/notifications.twig | 79 ++ templates/admin/users.twig | 70 + templates/alerts/index.twig | 60 + templates/dashboard.twig | 125 ++ templates/groups/create.twig | 53 + templates/groups/edit.twig | 54 + templates/groups/index.twig | 74 + templates/groups/show.twig | 97 ++ templates/layout.twig | 119 ++ templates/login-layout.twig | 50 + templates/login.twig | 33 + templates/servers/create.twig | 54 + templates/servers/created.twig | 65 + templates/servers/detail.twig | 456 +++++++ templates/servers/edit.twig | 74 + templates/servers/index.twig | 82 ++ templates/test.twig | 19 + 49 files changed, 5950 insertions(+) create mode 100644 AGENTS.md create mode 100755 INSTALL.md create mode 100755 TECHNICAL_SPECIFICATION.md create mode 100755 composer.json create mode 100755 composer.lock create mode 100755 config/DatabaseConfig.php create mode 100644 public/debug-login.php create mode 100644 public/index.php create mode 100644 public/index.php.broken create mode 100644 public/login-direct.php create mode 100644 public/session_check.php create mode 100644 public/session_test.php create mode 100644 public/set_session.php create mode 100755 schema.sql create mode 100755 src/Controllers/AdminController.php create mode 100755 src/Controllers/AgentController.php create mode 100755 src/Controllers/AlertController.php create mode 100755 src/Controllers/Api/MetricsController.php create mode 100644 src/Controllers/DashboardController.php create mode 100644 src/Controllers/GroupController.php create mode 100755 src/Controllers/ServerController.php create mode 100755 src/Controllers/ServerDetailController.php create mode 100755 src/Controllers/ServerDetailController.php.broken create mode 100755 src/Middlewares/AuthMiddleware.php create mode 100644 src/Middlewares/CsrfMiddleware.php create mode 100644 src/Middlewares/SessionMiddleware.php create mode 100644 src/Models/Alert.php create mode 100644 src/Models/Group.php create mode 100755 src/Models/Model.php create mode 100644 src/Models/Server.php create mode 100755 src/Models/User.php create mode 100644 src/Utils/EncryptionHelper.php create mode 100755 templates/admin/notifications.twig create mode 100755 templates/admin/users.twig create mode 100755 templates/alerts/index.twig create mode 100755 templates/dashboard.twig create mode 100755 templates/groups/create.twig create mode 100755 templates/groups/edit.twig create mode 100755 templates/groups/index.twig create mode 100644 templates/groups/show.twig create mode 100644 templates/layout.twig create mode 100644 templates/login-layout.twig create mode 100644 templates/login.twig create mode 100755 templates/servers/create.twig create mode 100755 templates/servers/created.twig create mode 100755 templates/servers/detail.twig create mode 100755 templates/servers/edit.twig create mode 100755 templates/servers/index.twig create mode 100755 templates/test.twig diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..6f48c21 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,124 @@ +# AGENTS.md - Инструкции для агентов по работе с проектом мониторинга + +## О проекте + +**Название:** Система мониторинга серверов +**URL:** https://mon.mirv.top +**Расположение:** /var/www/mon +**Технологии:** PHP 8.1+, Slim Framework 4, Twig, MySQL/MariaDB + +## Структура проекта + +``` +/var/www/mon/ +├── composer.json # Зависимости проекта +├── composer.lock # Зафиксированные версии зависимостей +├── .env # Конфигурация окружения +├── public/ +│ ├── index.php # Точка входа +│ ├── css/ # Стили +│ └── js/ # JavaScript +├── src/ +│ ├── Controllers/ # Контроллеры +│ ├── Middleware/ # Промежуточное ПО +│ ├── Models/ # Модели данных +│ └── Services/ # Бизнес-логика +├── templates/ # Шаблоны Twig +├── migrations/ # Миграции базы данных +└── tests/ # Тесты +``` + +## Зависимости + +- PHP 8.1+ +- Slim Framework 4 +- Twig template engine +- MySQL/MariaDB +- Composer для управления зависимостями + +## Конфигурация + +Файл `.env` содержит: +- DATABASE_URL - строка подключения к базе данных +- JWT_SECRET - секрет для JWT токенов +- SMTP настройки - для отправки уведомлений +- API токены для агентов мониторинга + +## API endpoints + +- `GET /api/servers` - список мониторинговых серверов +- `GET /api/servers/{id}` - детали сервера +- `POST /api/servers` - создание нового сервера +- `PUT /api/servers/{id}` - обновление сервера +- `DELETE /api/servers/{id}` - удаление сервера +- `GET /api/metrics/{server_id}` - метрики сервера +- `POST /api/agent/metrics` - получение метрик от агента +- `GET /csrf-token` - получение CSRF токена для форм + +## Агентские задачи + +### 1. Добавление нового сервера для мониторинга + +1. Использовать форму в `/servers/create` +2. Убедиться, что CSRF токен добавлен к форме (через `/csrf-token`) +3. Заполнить поля: название, IP-адрес, порт, токен агента +4. Проверить, что сервер отвечает на запросы + +### 2. Обновление конфигурации уведомлений + +1. Перейти в настройки уведомлений +2. Обновить email или webhook URL +3. Проверить доставку уведомлений + +### 3. Управление группами серверов + +1. Использовать `/groups` для создания/редактирования групп +2. Назначать серверы в группы +3. Устанавливать правила уведомлений на уровне групп + +### 4. Обновление системы + +1. Сделать резервную копию базы данных +2. Обновить зависимости через composer +3. Выполнить миграции базы данных +4. Обновить конфигурацию nginx при необходимости + +## Безопасность + +- Все формы требуют CSRF токены +- Аутентификация через сессии +- Валидация входных данных +- Санитизация вывода в шаблонах + +## Инструменты для агентов + +- Использовать `composer` для управления зависимостями +- Запускать миграции через `php vendor/bin/phinx` +- Тестировать через `phpunit` при наличии + +## Часто используемые команды + +```bash +# Установка зависимостей +composer install + +# Обновление зависимостей +composer update + +# Выполнение миграций +php vendor/bin/phinx migrate + +# Запуск тестов +php vendor/bin/phpunit + +# Резервное копирование БД +mysqldump -u username -p database_name > backup.sql +``` + +## Особенности архитектуры + +- MVC паттерн с использованием Slim Framework +- Аутентификация через сессии +- Middleware для защиты маршрутов +- Twig для безопасного рендеринга шаблонов +- Поддержка агентов мониторинга через API \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md new file mode 100755 index 0000000..1bd2d17 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,149 @@ +# Установка и запуск системы мониторинга + +## Требования + +- PHP 8.1 или выше +- Composer +- MySQL 8+ или MariaDB 10.5+ +- Apache или Nginx + +## Установка + +### 1. Клонирование проекта + +```bash +git clone +cd monitoring-system +``` + +### 2. Установка зависимостей + +```bash +composer install +``` + +### 3. Настройка базы данных + +1. Создайте базу данных: + +```sql +CREATE DATABASE monitoring_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +2. Импортируйте схему: + +```bash +mysql -u root -p monitoring_system < schema.sql +``` + +### 4. Настройка веб-сервера + +#### Apache + +Создайте виртуальный хост: + +```apache + + ServerName mon.mirv.top + DocumentRoot /path/to/monitoring-system/public + + + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/mon_error.log + CustomLog ${APACHE_LOG_DIR}/mon_access.log combined + +``` + +#### Nginx + +```nginx +server { + listen 80; + server_name mon.mirv.top; + root /path/to/monitoring-system/public; + index index.php; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location ~ \.php$ { + include snippets/fastcgi-php.conf; + fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + } +} +``` + +### 5. Настройка конфигурации базы данных + +Отредактируйте файл `config/DatabaseConfig.php` для указания параметров подключения к базе данных: + +```php +private $host = 'localhost'; +private $db_name = 'monitoring_system'; +private $username = 'your_db_username'; +private $password = 'your_db_password'; +``` + +## Использование + +### 1. Вход в систему + +Перейдите на `http://mon.mirv.top/login` и войдите в систему. + +Для первоначальной настройки создайте администратора через SQL: + +```sql +INSERT INTO users (username, password_hash, email, role) +VALUES ('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@example.com', 'admin'); +``` + +Пароль: `password` + +### 2. Добавление серверов + +1. Перейдите в раздел "Серверы" +2. Нажмите "Добавить сервер" +3. Заполните форму и сохраните +4. На странице подтверждения скопируйте токен и скачайте скрипт установки + +### 3. Установка агента на мониторимый сервер + +1. Скачайте скрипт установки с сервера мониторинга +2. Загрузите его на сервер, который нужно мониторить +3. Выполните: + +```bash +chmod +x install.sh +./install.sh +``` + +Агент будет установлен как systemd-сервис и начнет отправлять метрики на сервер мониторинга. + +## API + +### Отправка метрик + +Агенты отправляют метрики на эндпоинт `/api/v1/metrics` методом POST: + +```json +{ + "token": "токен_сервера", + "metrics": { + "cpu_load": 45.2, + "ram_used": 89.1, + "disk_used": 65.5 + } +} +``` + +## Безопасность + +- Все пароли хешируются с помощью `password_hash()` +- Токены агентов хранятся в виде SHA-256 хешей +- Все SQL-запросы используют подготовленные выражения +- Вход на все страницы требует аутентификации (кроме API и страницы входа) \ No newline at end of file diff --git a/TECHNICAL_SPECIFICATION.md b/TECHNICAL_SPECIFICATION.md new file mode 100755 index 0000000..f8fb49b --- /dev/null +++ b/TECHNICAL_SPECIFICATION.md @@ -0,0 +1,183 @@ +# ТЕХНИЧЕСКОЕ ЗАДАНИЕ: Система мониторинга серверов + +## 1. Общие требования +- **Язык:** PHP 8.1+ +- **Фреймворк:** Slim Framework 4 (с PSR-7, Twig) +- **Фронтенд:** Bootstrap 5, Font Awesome 6 (через CDN), Chart.js для графиков +- **База данных:** MySQL 8+ / MariaDB 10.5+ +- **Архитектура:** MVC в рамках Slim (Controllers, Models, Templates) +- **Интерфейс:** **Полностью на русском языке** (все надписи, кнопки, формы, меню) +- **Безопасность:** Prepared Statements для всех SQL-запросов, хеши паролей (`password_hash`), **токены агентов хранятся только как SHA-256 хеши**. + +## 2. Структура базы данных (8 таблиц) +| Таблица | Поля (ключевые) | Назначение | +| ------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| **users** | `id, username (unique), password_hash, email, role(admin/user), created_at` | Управление доступом | +| **user_notification_settings** | `id, user_id, telegram_chat_id, email_for_alerts` | Персональные настройки уведомлений | +| **server_groups** | `id, name, description, icon, color` | Группировка серверов | +| **servers** | `id, name, address (nullable), group_id, description, last_metrics_at, created_at` | Основные данные серверов | +| **metric_names** | `id, name (unique), unit, description` | Справочник метрик (cpu_load, ram_used, …) | +| **metric_thresholds** | `id, server_id, metric_name_id, warning_threshold, critical_threshold, duration` | **Индивидуальные пороги** для каждой метрики на каждом сервере | +| **server_metrics** | `id, server_id, metric_name_id, value, created_at` | **Исторические данные** метрик (с индексом `(server_id, metric_name_id, created_at)`) | +| **agent_tokens** | `id, server_id (unique), token_hash, created_at, last_used_at` | **Хеши** токенов для авторизации агентов | +| **alerts** | `id, server_id, metric_name, value, severity(warning/critical), resolved, created_at, resolved_at` | История срабатывания алертов | + +## 3. Функциональные модули (что должна делать система) + +### 3.1. Аутентификация и авторизация +- Доступ ко всем страницам, кроме `/login` и `/api/v1/metrics`, **только после входа**. +- Сессионная авторизация с middleware. + +### 3.2. Дашборд (главная страница `/`) +- **Цветные карточки серверов** (Bootstrap cards): + - **Зеленый:** `last_metrics_at` < 2 мин назад И все метрики ниже порогов. + - **Желтый:** метрики свежие (<2 мин), но есть превышение порогов. + - **Красный:** `last_metrics_at` > 5 мин назад (сервер «молчит»). +- В карточке: имя сервера, текущие значения CPU/RAM, время последнего обновления. +- **Автообновление** каждые 30 секунд (через `setTimeout`). + +### 3.3. Управление серверами +- **Добавление сервера:** форма с полями (имя, адрес опционально, группа, описание). +- **После сохранения:** + 1. Генерация **уникального токена** (32 символа). + 2. Сохранение **хеша токена** в `agent_tokens`. + 3. Показ пользователю: **«Токен для агента: [токен]. Скачайте скрипт установки: [install.sh?token=...]»**. +- **Редактирование сервера:** возможность изменить все поля, **повторно скачать скрипт** (токен остается прежним). + +### 3.4. API для агентов +- **Эндпоинт:** `POST /api/v1/metrics` (публичный, без авторизации сессии). +- **Ожидаемый JSON:** `{"token": "...", "metrics": {"cpu_load": 45.2, "ram_used": 89.1}}`. +- **Логика:** + 1. Проверка токена (сравнение хеша). + 2. Обновление `servers.last_metrics_at` и `agent_tokens.last_used_at`. + 3. Сохранение каждой метрики в `server_metrics`. + 4. **Проверка порогов:** если значение превышает `warning_threshold` из `metric_thresholds` → создание записи в `alerts`. + +### 3.5. Агент мониторинга +- **Скрипт установки:** `GET /agent/install.sh?token=...` → динамически генерирует **bash-скрипт**, который: + 1. Устанавливает Python3, psutil. + 2. Скачивает Python-агента (`agent.py`) с вашего сервера. + 3. Подставляет **токен** и **URL API** в конфиг агента. + 4. Создает systemd-сервис для автостарта. +- **Python-агент:** собирает CPU, RAM, Disk раз в **10 секунд**, отправляет на `/api/v1/metrics`. + +### 3.6. Страница сервера (`/server/{id}`) +- **Текущие значения** всех метрик. +- **Графики Chart.js** для метрик: + - По умолчанию — **последние 24 часа**. + - Кнопки выбора периода: **24 часа, 7 дней, 30 дней** (через параметр `?period=7d`). + - **Управление порогами:** форма настройки `warning_threshold` и `duration` для каждой метрики этого сервера (сохраняет в `metric_thresholds`). + +### 3.7. Система алертов +- **Автоматическое создание** при превышении порога. +- **Страница `/alerts`:** список активных алертов с кнопкой **«Исправлено»** (помечает `resolved=1`). +- **Логика статуса на дашборде** учитывает только **неисправленные (resolved=0)** алерты. + +### 3.8. Уведомления +- **Email:** отправка через SMTP (PHPMailer) при срабатывании алерта. +- **Telegram Bot:** отправка сообщения в заданный чат. +- **Настройка:** в админке (`/admin/notifications`) указываются: SMTP параметры, Telegram Bot Token, Chat ID. + +### 3.9. Администрирование +- **CRUD пользователей:** страница `/admin/users` (только для `role='admin'`). +- **Управление группами серверов.** +- **Повторная выдача скрипта:** на странице сервера кнопка **«Скачать скрипт агента»**. + +--- + +## План поэтапной реализации (9 этапов) + +### Этап 1: Ядро системы +- **Цель:** Slim Framework + Twig + Bootstrap работают. +- **Результат:** Открываю `http://localhost:8080/test` → вижу «Система мониторинга». +- ✅ **Выполнен** + +### Этап 2: База данных +- **Цель:** Все 8 таблиц созданы. +- **Результат:** Импорт `schema.sql` → в MySQL есть все таблицы, включая `metric_names` с записями «cpu_load», «ram_used». +- ✅ **Выполнен** + +### Этап 3: Аутентификация + CRUD групп +- **Цель:** Вход/выход, управление группами. +- **Результат:** + 1. Неавторизован → редирект на `/login` (форма на русском). + 2. Вход как `admin:123` → переход на дашборд. + 3. Меню «Группы» → создаю группу «Веб-серверы» → она отображается в списке. +- ✅ **Выполнен** + +### Этап 4: CRUD серверов + генерация токена +- **Цель:** Добавление сервера с получением токена. +- **Результат:** + 1. Форма «Добавить сервер» → заполняю → нажимаю «Сохранить». + 2. Вижу страницу: **«Сервер добавлен. Токен: abc123... Скачайте скрипт: [install.sh?token=abc123...]»**. + 3. Перехожу по ссылке → скачивается `install.sh`. +- ✅ **Выполнен** + +### Этап 5: API + логика статусов +- **Цель:** Приём метрик, цветные карточки на дашборде. +- **Результат:** + 1. Через `curl` отправляю метрики с токеном. + 2. Обновляю дашборд → вижу карточку сервера **зелёного цвета** (метрики в норме). + 3. Отправляю `cpu_load: 95` → карточка становится **жёлтой**. +- ✅ **Выполнен** + +### Этап 6: Агент и скрипт установки +- **Цель:** Рабочий агент, отправляющий метрики каждые 10 сек. +- **Результат:** + 1. В виртуальной машине запускаю `bash install.sh` → устанавливается Python-агент. + 2. `systemctl status server-mon-agent` → сервис работает. + 3. В логах агента вижу: «Отправлено: cpu_load=12.3» каждые 10 сек. +- ✅ **Выполнен** + +### Этап 7: Детали сервера + графики +- **Цель:** Страница с графиками и выбором периода. +- **Результат:** + 1. Кликаю на карточку сервера → открывается `/server/1`. + 2. Вижу график CPU за 24 часа. + 3. Нажимаю «7 дней» → график перерисовывается за неделю. +- ✅ **Выполнен** + +### Этап 8: Пороги и алерты +- **Цель:** Настройка порогов, срабатывание алертов. +- **Результат:** + 1. На странице сервера ставлю порог CPU: 80%. + 2. Отправляю метрику `cpu_load: 90` → в таблице `alerts` появляется запись. + 3. Страница `/alerts` показывает «Превышение CPU на сервере X». +- ✅ **Выполнен** + +### Этап 9: Уведомления + админка +- **Цель:** Отправка email/Telegram, управление пользователями. +- **Результат:** + 1. В админке указываю свой Telegram Chat ID. + 2. При срабатывании алерта получаю сообщение в Telegram. + 3. В меню «Админка» → «Пользователи» создаю нового пользователя. +- ✅ **Выполнен** + +--- + +## История реализации + +### Выполненные этапы: +- ✅ Этап 1: Ядро системы (Slim Framework 4, Twig, Bootstrap 5, Font Awesome 6) +- ✅ Этап 2: База данных (схема создана, 8 таблиц, стандартные метрики) +- ✅ Этап 3: Аутентификация и авторизация (middleware, формы входа/выхода, CRUD групп) +- ✅ Этап 4: CRUD серверов (добавление/редактирование/удаление, генерация токенов) +- ✅ Этап 5: API для агентов (прием метрик, проверка токенов, обновление статусов) +- ✅ Этап 6: Скрипт установки агента (динамическая генерация bash-скрипта) +- ✅ Этап 7: Детали сервера и графики (страница с метриками и графиками Chart.js) +- ✅ Этап 8: Система алертов (обнаружение превышений, страница алертов) +- ✅ Этап 9: Администрирование (управление пользователями, настройки уведомлений) + +### Архитектура: +- **Frontend:** Bootstrap 5, Font Awesome 6, Chart.js (через CDN) +- **Backend:** Slim Framework 4, Twig templates, PSR-7 +- **Database:** MySQL/MariaDB с 8 таблицами +- **Security:** Password hashing, SHA-256 token hashes, prepared statements +- **Language:** Полный перевод интерфейса на русский язык + +### Компоненты: +- Веб-интерфейс для управления серверами и мониторинга +- API для приема метрик от агентов +- Скрипт установки агента для мониторинга серверов +- Система алертов и уведомлений +- Административная панель \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100755 index 0000000..387c40b --- /dev/null +++ b/composer.json @@ -0,0 +1,26 @@ +{ + "name": "mirivlad/monitoring-system", + "description": "Система мониторинга серверов", + "type": "project", + "require": { + "php": "^8.1", + "slim/slim": "^4.12", + "slim/psr7": "^1.6", + "slim/twig-view": "^3.3", + "twig/twig": "^3.0", + "ext-pdo": "*", + "slim/csrf": "^1.3" + }, + "autoload": { + "psr-4": { + "App\\": "src/", + "Config\\": "config/" + } + }, + "scripts": { + "start": "php -S localhost:8080 -t public" + }, + "config": { + "process-timeout": 0 + } +} diff --git a/composer.lock b/composer.lock new file mode 100755 index 0000000..d3653d6 --- /dev/null +++ b/composer.lock @@ -0,0 +1,1206 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "efe8e92fac5b5dfd49e91a84351814d5", + "packages": [ + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "slim/csrf", + "version": "1.5.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Csrf.git", + "reference": "a476a61e38451e138c400f6b4ca96037f3c2dd39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Csrf/zipball/a476a61e38451e138c400f6b4ca96037f3c2dd39", + "reference": "a476a61e38451e138c400f6b4ca96037f3c2dd39", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6", + "squizlabs/php_codesniffer": "^3.10" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Csrf\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + } + ], + "description": "Slim Framework 4 CSRF protection PSR-15 middleware", + "homepage": "https://www.slimframework.com", + "keywords": [ + "csrf", + "framework", + "middleware", + "slim" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Csrf/issues", + "source": "https://github.com/slimphp/Slim-Csrf/tree/1.5.1" + }, + "time": "2025-11-02T14:58:28+00:00" + }, + { + "name": "slim/psr7", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Psr7.git", + "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/76e7e3b1cdfd583e9035c4c966c08e01e45ce959", + "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "php": "^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.0 || ^2.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.5|| ^2.0", + "ext-json": "*", + "http-interop/http-factory-tests": "^1.0 || ^2.0", + "php-http/psr7-integration-tests": "^1.5", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6 || ^10", + "squizlabs/php_codesniffer": "^3.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Psr7\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + } + ], + "description": "Strict PSR-7 implementation", + "homepage": "https://www.slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Psr7/issues", + "source": "https://github.com/slimphp/Slim-Psr7/tree/1.8.0" + }, + "time": "2025-11-02T17:51:19+00:00" + }, + { + "name": "slim/slim", + "version": "4.15.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "887893516557506f254d950425ce7f5387a26970" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/887893516557506f254d950425ce7f5387a26970", + "reference": "887893516557506f254d950425ce7f5387a26970", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4 || ^2", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.6", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17 || ^3", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.1", + "phpstan/phpstan": "^1 || ^2", + "phpunit/phpunit": "^9.6 || ^10 || ^11 || ^12", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.10", + "vimeo/psalm": "^5 || ^6" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2025-11-21T12:23:44+00:00" + }, + { + "name": "slim/twig-view", + "version": "3.4.1", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Twig-View.git", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Twig-View/zipball/b4268d87d0e327feba5f88d32031e9123655b909", + "reference": "b4268d87d0e327feba5f88d32031e9123655b909", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/http-message": "^1.1 || ^2.0", + "slim/slim": "^4.12", + "symfony/polyfill-php81": "^1.29", + "twig/twig": "^3.11" + }, + "require-dev": { + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/phpstan": "^1.10.59", + "phpunit/phpunit": "^9.6 || ^10", + "psr/http-factory": "^1.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Views\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "http://joshlockhart.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "http://www.lgse.com" + } + ], + "description": "Slim Framework 4 view helper built on top of the Twig 3 templating component", + "homepage": "https://www.slimframework.com", + "keywords": [ + "framework", + "slim", + "template", + "twig", + "view" + ], + "support": { + "issues": "https://github.com/slimphp/Twig-View/issues", + "source": "https://github.com/slimphp/Twig-View/tree/3.4.1" + }, + "time": "2024-09-26T05:42:02+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "twig/twig", + "version": "v3.23.0", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "shasum": "" + }, + "require": { + "php": ">=8.1.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-mbstring": "^1.3" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "psr/container": "^1.0|^2.0", + "symfony/phpunit-bridge": "^5.4.9|^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resources/core.php", + "src/Resources/debug.php", + "src/Resources/escaper.php", + "src/Resources/string_loader.php" + ], + "psr-4": { + "Twig\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Twig Team", + "role": "Contributors" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "https://twig.symfony.com", + "keywords": [ + "templating" + ], + "support": { + "issues": "https://github.com/twigphp/Twig/issues", + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" + }, + "funding": [ + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/twig/twig", + "type": "tidelift" + } + ], + "time": "2026-01-23T21:00:41+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1", + "ext-pdo": "*" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/DatabaseConfig.php b/config/DatabaseConfig.php new file mode 100755 index 0000000..ea73a28 --- /dev/null +++ b/config/DatabaseConfig.php @@ -0,0 +1,44 @@ +host};dbname={$this->db_name};charset={$this->charset}"; + $options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]; + + try { + $this->connection = new PDO($dsn, $this->username, $this->password, $options); + } catch (PDOException $e) { + throw new PDOException($e->getMessage(), (int)$e->getCode()); + } + } + + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self(); + } + + return self::$instance->connection; + } +} \ No newline at end of file diff --git a/public/debug-login.php b/public/debug-login.php new file mode 100644 index 0000000..d57220c --- /dev/null +++ b/public/debug-login.php @@ -0,0 +1,57 @@ +authenticate($username, $password); + + if ($user) { + echo "DEBUG: Auth successful!\n"; + + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + + header('Location: /'); + exit; + } else { + echo "DEBUG: Auth failed!\n"; + header('Location: /login'); + exit; + } +} +?> + + +

Debug Mode

+

Check /tmp/login_debug.log for details

+ + diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..0091c0b --- /dev/null +++ b/public/index.php @@ -0,0 +1,207 @@ +getResponseFactory()); +$csrf->setPersistentTokenMode(true); + +// Create Twig view +$twig = Twig::create(__DIR__ . '/../templates', ['cache' => false]); + +// Add CSRF middleware FIRST +$app->add($csrf); + +// Add Twig middleware +$twigMiddleware = TwigMiddleware::create($app, $twig); +$app->add($twigMiddleware); + +// Add session middleware (MUST be after TwigMiddleware) +$sessionMiddleware = new SessionMiddleware($twig); +$app->add($sessionMiddleware); + +// Add a route to get CSRF tokens via AJAX +$app->get('/csrf-token', function (Request $request, Response $response, $args) use ($csrf) { + $data = [ + 'name_key' => $csrf->getTokenNameKey(), + 'value_key' => $csrf->getTokenValueKey(), + 'name' => $csrf->getTokenName(), + 'value' => $csrf->getTokenValue() + ]; + + $response->getBody()->write(json_encode($data)); + return $response->withHeader('Content-Type', 'application/json'); +}); + +// Define /test route +$app->get('/test', function (Request $request, Response $response, $args) use ($twig) { + $templateData = [ + 'title' => 'Тест системы', + 'message' => 'Система мониторинга запущена' + ]; + + return $twig->render($response, 'test.twig', $templateData); +}); + +// Login routes (without auth middleware, but with CSRF) +$app->get('/login', function (Request $request, Response $response, $args) use ($twig, $csrf) { + $templateData = [ + 'title' => 'Вход в систему', + 'csrf' => [ + 'name_key' => $csrf->getTokenNameKey(), + 'value_key' => $csrf->getTokenValueKey(), + 'name' => $csrf->getTokenName(), + 'value' => $csrf->getTokenValue() + ] + ]; + + return $twig->render($response, 'login.twig', $templateData); +}); + +$app->post('/login', function (Request $request, Response $response, $args) use ($csrf) { + $params = $request->getParsedBody(); + + // Validate CSRF token + $nameKey = $csrf->getTokenNameKey(); + $valueKey = $csrf->getTokenValueKey(); + + if (!isset($params[$nameKey]) || !isset($params[$valueKey]) || !$csrf->validateToken($params[$nameKey], $params[$valueKey])) { + error_log('CSRF validation failed for /login'); + return $response->withHeader('Location', '/login')->withStatus(302); + } + + $username = $params['username'] ?? ''; + $password = $params['password'] ?? ''; + + $userModel = new User(); + $user = $userModel->authenticate($username, $password); + + if ($user) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + + return $response->withHeader('Location', '/')->withStatus(302); + } else { + return $response->withHeader('Location', '/login')->withStatus(302); + } +}); + +// Logout route (without auth middleware) +$app->get('/logout', function (Request $request, Response $response, $args) { + session_destroy(); + return $response->withHeader('Location', '/login')->withStatus(302); +}); + +// Dashboard route (protected with auth middleware) +$app->get('/', function (Request $request, Response $response, $args) use ($twig) { + $serverModel = new ServerModel(); + + // Get statistics + $stats = $serverModel->getStats(); + + // Get servers with latest metrics + $servers = $serverModel->getAll(); + + $templateData = [ + 'title' => 'Дашборд мониторинга', + 'stats' => $stats, + 'servers' => $servers + ]; + + return $twig->render($response, 'dashboard.twig', $templateData); +})->add(AuthMiddleware::class); + +// Create controllers BEFORE routes +$groupController = new GroupController($twig); +$serverController = new ServerController($twig); +$serverDetailController = new ServerDetailController($twig); +$alertController = new AlertController($twig); +$adminController = new AdminController($twig); +$metricsController = new MetricsController(); +$agentController = new AgentController(); + +// Routes for groups (protected with auth middleware) +$app->get('/groups', [$groupController, 'index'])->add(AuthMiddleware::class); +$app->get('/groups/create', [$groupController, 'create'])->add(AuthMiddleware::class); +$app->post('/groups', [$groupController, 'store'])->add(AuthMiddleware::class); +$app->get('/groups/{id}/edit', [$groupController, 'edit'])->add(AuthMiddleware::class); +$app->post('/groups/{id}', [$groupController, 'update'])->add(AuthMiddleware::class); +$app->delete('/groups/{id}', [$groupController, 'delete'])->add(AuthMiddleware::class); +$app->get('/groups/{id}', [$groupController, 'show'])->add(AuthMiddleware::class); + +// Routes for servers (protected with auth middleware) +$app->get('/servers', [$serverController, 'index'])->add(AuthMiddleware::class); +$app->get('/servers/create', [$serverController, 'create'])->add(AuthMiddleware::class); +$app->post('/servers', [$serverController, 'store'])->add(AuthMiddleware::class); +$app->get('/servers/{id}/edit', [$serverController, 'edit'])->add(AuthMiddleware::class); +$app->post('/servers/{id}', [$serverController, 'update'])->add(AuthMiddleware::class); +$app->delete('/servers/{id}', [$serverController, 'delete'])->add(AuthMiddleware::class); +$app->get('/servers/{id}/regenerate-token', [$serverController, 'regenerateToken'])->add(AuthMiddleware::class); +$app->post('/servers/{id}/thresholds', [$serverDetailController, 'saveThresholds'])->add(AuthMiddleware::class); +$app->post('/servers/{id}/services', [$serverDetailController, 'saveServices'])->add(AuthMiddleware::class); + +// Server detail route (protected with auth middleware) +$app->get('/servers/{id}', [$serverDetailController, 'show'])->add(AuthMiddleware::class); + +// Alerts routes (protected with auth middleware) +$app->get('/alerts', [$alertController, 'index'])->add(AuthMiddleware::class); +$app->get('/alerts/{id}/resolve', [$alertController, 'markAsResolved'])->add(AuthMiddleware::class); + +// Admin routes (protected with auth middleware) +$app->get('/admin/users', [$adminController, 'usersList'])->add(AuthMiddleware::class); +$app->get('/admin/notifications', [$adminController, 'notificationSettings'])->add(AuthMiddleware::class); + +// API route for agents (public, no auth middleware) +$app->post('/api/v1/metrics', [$metricsController, 'collectMetrics']); +$app->get("/api/v1/agent/{id}/services", [$metricsController, 'getServices'])->add(AuthMiddleware::class); + +// Agent configuration routes (protected with auth middleware) +$app->get("/agent/{id}/config", [$agentController, 'getConfig'])->add(AuthMiddleware::class); +$app->post("/agent/{id}/config", [$agentController, 'updateConfig'])->add(AuthMiddleware::class); +$app->get("/agent/{id}/status", [$agentController, 'getStatus'])->add(AuthMiddleware::class); + +// API status endpoint (public, no auth middleware) +$app->get('/api/status', function (Request $request, Response $response, $args) { + $data = [ + 'status' => 'ok', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0.0' + ]; + + $response->getBody()->write(json_encode($data)); + return $response + ->withHeader('Content-Type', 'application/json'); +}); + +// Agent installation script route (public, no auth middleware) +$app->get('/agent/install.sh', [$agentController, 'generateInstallScript']); + +// Run app +$app->run(); diff --git a/public/index.php.broken b/public/index.php.broken new file mode 100644 index 0000000..63a1c50 --- /dev/null +++ b/public/index.php.broken @@ -0,0 +1,202 @@ +getResponseFactory()); +$csrf->setPersistentTokenMode(true); + +// Create Twig view +$twig = Twig::create(__DIR__ . '/../templates', ['cache' => false]); + +// Add CSRF middleware FIRST +$app->add($csrf); + +// Add Twig middleware +$twigMiddleware = TwigMiddleware::create($app, $twig); +$app->add($twigMiddleware); +$sessionMiddleware = new AppMiddlewaresSessionMiddleware($twig); +$app->add($sessionMiddleware); +// Add session data to Twig$sessionMiddleware = new SessionMiddleware($twig);$app->add($sessionMiddleware); + +// Add a route to get CSRF tokens via AJAX +$app->get('/csrf-token', function (Request $request, Response $response, $args) use ($csrf) { + $data = [ + 'name_key' => $csrf->getTokenNameKey(), + 'value_key' => $csrf->getTokenValueKey(), + 'name' => $csrf->getTokenName(), + 'value' => $csrf->getTokenValue() + ]; + + $response->getBody()->write(json_encode($data)); + return $response->withHeader('Content-Type', 'application/json'); +}); + +// Define /test route +$app->get('/test', function (Request $request, Response $response, $args) use ($twig) { + $templateData = [ + 'title' => 'Тест системы', + 'message' => 'Система мониторинга запущена' + ]; + + return $twig->render($response, 'test.twig', $templateData); +}); + +// Login routes (without auth middleware, but with CSRF) +$app->get('/login', function (Request $request, Response $response, $args) use ($twig, $csrf) { + $templateData = [ + 'title' => 'Вход в систему', + 'csrf' => [ + 'name_key' => $csrf->getTokenNameKey(), + 'value_key' => $csrf->getTokenValueKey(), + 'name' => $csrf->getTokenName(), + 'value' => $csrf->getTokenValue() + ] + ]; + + return $twig->render($response, 'login.twig', $templateData); +}); + +$app->post('/login', function (Request $request, Response $response, $args) use ($csrf) { + $params = $request->getParsedBody(); + + // Validate CSRF token + $nameKey = $csrf->getTokenNameKey(); + $valueKey = $csrf->getTokenValueKey(); + + if (!isset($params[$nameKey]) || !isset($params[$valueKey]) || !$csrf->validateToken($params[$nameKey], $params[$valueKey])) { + error_log('CSRF validation failed for /login'); + return $response->withHeader('Location', '/login')->withStatus(302); + } + + $username = $params['username'] ?? ''; + $password = $params['password'] ?? ''; + + $userModel = new User(); + $user = $userModel->authenticate($username, $password); + + if ($user) { + $_SESSION['user_id'] = $user['id']; + $_SESSION['username'] = $user['username']; + $_SESSION['role'] = $user['role']; + + return $response->withHeader('Location', '/')->withStatus(302); + } else { + return $response->withHeader('Location', '/login')->withStatus(302); + } +}); + +// Logout route (without auth middleware) +$app->get('/logout', function (Request $request, Response $response, $args) { + session_destroy(); + return $response->withHeader('Location', '/login')->withStatus(302); +}); + +// Dashboard route (protected with auth middleware) +$app->get('/', function (Request $request, Response $response, $args) use ($twig) { + $serverModel = new ServerModel(); + + // Get statistics + $stats = $serverModel->getStats(); + + // Get servers with latest metrics + $servers = $serverModel->getAll(); + + $templateData = [ + 'title' => 'Дашборд мониторинга', + 'stats' => $stats, + 'servers' => $servers + ]; + + return $twig->render($response, 'dashboard.twig', $templateData); +})->add(AuthMiddleware::class); + +// Routes for groups (protected with auth middleware) +$groupController = new GroupController($twig); + +$app->get('/groups', [$groupController, 'index'])->add(AuthMiddleware::class); +$app->get('/groups/create', [$groupController, 'create'])->add(AuthMiddleware::class); +$app->post('/groups', [$groupController, 'store'])->add(AuthMiddleware::class); +$app->get('/groups/{id}/edit', [$groupController, 'edit'])->add(AuthMiddleware::class); +$app->post('/groups/{id}', [$groupController, 'update'])->add(AuthMiddleware::class); +$app->delete('/groups/{id}', [$groupController, 'delete'])->add(AuthMiddleware::class); + +// Routes for servers (protected with auth middleware) +$serverController = new ServerController($twig); + +$app->get('/servers', [$serverController, 'index'])->add(AuthMiddleware::class); +$app->get('/servers/create', [$serverController, 'create'])->add(AuthMiddleware::class); +$app->post('/servers', [$serverController, 'store'])->add(AuthMiddleware::class); +$app->get('/servers/{id}/edit', [$serverController, 'edit'])->add(AuthMiddleware::class); +$app->post('/servers/{id}', [$serverController, 'update'])->add(AuthMiddleware::class); +$app->delete('/servers/{id}', [$serverController, 'delete'])->add(AuthMiddleware::class); +$app->get('/servers/{id}/regenerate-token', [$serverController, 'regenerateToken'])->add(AuthMiddleware::class); + +// Server detail route (protected with auth middleware) +$serverDetailController = new ServerDetailController($twig); + +$app->get('/servers/{id}', [$serverDetailController, 'show'])->add(AuthMiddleware::class); + +// Alerts routes (protected with auth middleware) +$alertController = new AlertController($twig); + +$app->get('/alerts', [$alertController, 'index'])->add(AuthMiddleware::class); +$app->get('/alerts/{id}/resolve', [$alertController, 'markAsResolved'])->add(AuthMiddleware::class); + +// Admin routes (protected with auth middleware) +$adminController = new AdminController($twig); + +$app->get('/admin/users', [$adminController, 'usersList'])->add(AuthMiddleware::class); +$app->get('/admin/notifications', [$adminController, 'notificationSettings'])->add(AuthMiddleware::class); + +// API route for agents (public, no auth middleware) +$metricsController = new MetricsController(); + +$app->post('/api/v1/metrics', [$metricsController, 'collectMetrics']); + +// API status endpoint (public, no auth middleware) +$app->get('/api/status', function (Request $request, Response $response, $args) { + $data = [ + 'status' => 'ok', + 'timestamp' => date('Y-m-d H:i:s'), + 'version' => '1.0.0' + ]; + + $response->getBody()->write(json_encode($data)); + return $response + ->withHeader('Content-Type', 'application/json'); +}); + +// Agent installation script route (public, no auth middleware) +$agentController = new AgentController(); + +$app->get('/agent/install.sh', [$agentController, 'generateInstallScript']); + +// Run app +$app->run(); diff --git a/public/login-direct.php b/public/login-direct.php new file mode 100644 index 0000000..29726ad --- /dev/null +++ b/public/login-direct.php @@ -0,0 +1,46 @@ + + + + + Тест входа + + + +

Тестовый вход

+
+

+ Логин: +

+

+ Пароль: +

+

+ +

+
+

Тестовые креды: admin / admin_test_2026

+ + diff --git a/public/session_check.php b/public/session_check.php new file mode 100644 index 0000000..135a837 --- /dev/null +++ b/public/session_check.php @@ -0,0 +1,12 @@ + session_id(), + 'user_id' => $_SESSION['user_id'] ?? null, + 'username' => $_SESSION['username'] ?? null, + 'role' => $_SESSION['role'] ?? null, + 'session_data' => $_SESSION +], JSON_PRETTY_PRINT); diff --git a/public/session_test.php b/public/session_test.php new file mode 100644 index 0000000..a7c1dc5 --- /dev/null +++ b/public/session_test.php @@ -0,0 +1,23 @@ +twig = $twig; + } + + public function usersList(Request $request, Response $response, $args) + { + // Только для администраторов + if ($_SESSION['role'] !== 'admin') { + return $response->withHeader('Location', '/')->withStatus(302); + } + + $stmt = $this->pdo->prepare("SELECT id, username, email, role, created_at FROM users ORDER BY created_at DESC"); + $stmt->execute(); + $users = $stmt->fetchAll(); + + $templateData = [ + 'title' => 'Управление пользователями', + 'users' => $users + ]; + + return $this->twig->render($response, 'admin/users.twig', $templateData); + } + + public function notificationSettings(Request $request, Response $response, $args) + { + // Только для администраторов + if ($_SESSION['role'] !== 'admin') { + return $response->withHeader('Location', '/')->withStatus(302); + } + + $templateData = [ + 'title' => 'Настройки уведомлений' + ]; + + return $this->twig->render($response, 'admin/notifications.twig', $templateData); + } +} \ No newline at end of file diff --git a/src/Controllers/AgentController.php b/src/Controllers/AgentController.php new file mode 100755 index 0000000..e13cf09 --- /dev/null +++ b/src/Controllers/AgentController.php @@ -0,0 +1,424 @@ +getQueryParams(); + $token = $queryParams['token'] ?? null; + $server_id = $queryParams['server_id'] ?? null; + + // Если передан server_id, получаем оригинальный токен из зашифрованного + if (!empty($server_id) && empty($token)) { + $stmt = $this->pdo->prepare("SELECT encrypted_token FROM agent_tokens WHERE server_id = :server_id LIMIT 1"); + $stmt->execute([':server_id' => $server_id]); + $result = $stmt->fetch(); + + if ($result && !empty($result['encrypted_token'])) { + $token = EncryptionHelper::decrypt($result['encrypted_token']); + } + } + + if (empty($token)) { + $response->getBody()->write('Token is required'); + return $response->withStatus(400); + } + + $apiUrl = 'https://mon.mirv.top/api/v1/metrics'; + + // Формируем скрипт с прямой подстановкой значений + $script = "#!/bin/bash + +# Скрипт установки агента мониторинга с поддержкой сервисов +# Сгенерировано автоматически + +TOKEN='" . $token . "' +API_URL='" . $apiUrl . "' + +echo 'Установка агента мониторинга...' + +# Проверяем наличие Python3 +if ! command -v python3 &> /dev/null; then + echo 'Установка Python3...' + apt-get update + apt-get install -y python3 python3-pip +fi + +# Устанавливаем psutil +pip3 install psutil || easy_install3 psutil + +# Создаем директорию для агента +mkdir -p /opt/server-monitor-agent +cd /opt/server-monitor-agent + +# Создаем конфигурационный файл +echo '{ + \\\"token\\\": \\\"" . $token . "\\\"\\, + \\\"api_url\\\": \\\"" . $apiUrl . "\\\"\\, + \\\"interval_seconds\\\": 60 +}' > config.json + +# Создаем Python-скрипт агента с поддержкой сервисов +cat > agent.py << 'PYTHON_EOF' +import time +import json +import psutil +import requests +import subprocess +import os +from datetime import datetime + +def get_metrics(): + \\\"\\\"\\\"Сбор системных метрик\\\"\\\"\\\" + cpu_percent = psutil.cpu_percent(interval=1) + memory = psutil.virtual_memory() + disk_usage = psutil.disk_usage('/') + + # Получаем сетевую статистику + try: + net_io = psutil.net_io_counters() + except: + net_io = None + + metrics = { + 'cpu_load': round(cpu_percent, 2), + 'ram_used': round(memory.percent, 2), + 'disk_used': round((disk_usage.used / disk_usage.total) * 100, 2), + 'network_in': round((net_io.bytes_recv / (1024*1024)) if net_io else 0, 2), # MB + 'network_out': round((net_io.bytes_sent / (1024*1024)) if net_io else 0, 2) # MB + } + + return metrics + +def get_services(): + \\\"\\\"\\\"Сбор статусов всех сервисов\\\"\\\"\\\" + services = [] + + try: + # Получаем список всех сервисов + result = subprocess.run( + ['systemctl', 'list-units', '--type=service', '--all', '--no-pager'], + capture_output=True, + text=True, + timeout=30 + ) + + lines = result.stdout.strip().split('\\n') + + for line in lines[1:]: # Пропускаем заголовок + parts = line.split() + if len(parts) >= 4: + service_name = parts[0].replace('.service', '') + load_state = parts[1] + active_state = parts[2] + sub_state = parts[3] if len(parts) > 3 else '' + + # Определяем статус сервиса + if active_state == 'active' and sub_state == 'running': + status = 'running' + elif active_state in ['inactive', 'failed', 'dead']: + status = 'stopped' + else: + status = 'unknown' + + # Пропускаем системные сервисы без .service в имени + if not service_name.startswith('system-'): + services.append({ + 'name': service_name, + 'status': status, + 'load_state': load_state, + 'active_state': active_state, + 'sub_state': sub_state + }) + + except Exception as e: + print(f'Ошибка при получении списка сервисов: {e}') + + return services + +def get_config_from_server(): + \\\"\\\"\\\"Получение конфигурации с сервера\\\"\\\"\\\" + try: + with open('config.json', 'r') as f: + config = json.load(f) + except Exception as e: + print(f'Ошибка чтения конфига: {e}') + return None + + token = config.get('token') + if not token: + print('Отсутствует токен в конфиге') + return None + + # Определяем URL для получения конфигурации + server_id = token.split('-')[0] if '-' in token else '1' + + try: + response = requests.get( + f\\\"\\\"{config['api_url']}/agent/{server_id}/config\\\"\\\"\\\", + headers={'Authorization': f'Bearer {token}'}, + timeout=10 + ) + + if response.status_code == 200: + server_config = response.json() + + # Обновляем локальный конфиг + config['interval_seconds'] = server_config.get('interval_seconds', config['interval_seconds']) + config['monitor_services'] = server_config.get('monitor_services', config.get('monitor_services', [])) + + # Сохраняем обновленный конфиг + with open('config.json', 'w') as f: + json.dump(config, f, indent=2) + + return config + else: + print(f'Ошибка получения конфига с сервера: {response.status_code}') + return config + + except Exception as e: + print(f'Ошибка подключения к серверу: {e}') + return config + +def send_metrics(config, metrics, services): + \\\"\\\"\\\"Отправка метрик и сервисов на сервер\\\"\\\"\\\" + data = { + 'token': config['token'], + 'metrics': metrics, + 'services': services + } + + try: + response = requests.post( + config['api_url'], + json=data, + timeout=10 + ) + if response.status_code == 200: + print(f'{datetime.now().strftime(\\\"%Y-%m-%d %H:%M:%S\\\")} - Метрики отправлены успешно') + return True + else: + print(f'Ошибка отправки метрик: {response.status_code}') + return False + except Exception as e: + print(f'Ошибка отправки метрик: {e}') + return False + +def main(): + \\\"\\\"\\\"Главная функция агента\\\"\\\"\\\" + print('Агент мониторинга запущен...') + + # Загружаем конфигурацию + config = get_config_from_server() + if not config: + print('Не удалось загрузить конфигурацию') + return + + interval = config.get('interval_seconds', 60) + monitor_services = config.get('monitor_services', []) + + print(f'Интервал отправки: {interval} сек') + print(f'Мониторинг сервисов: {\\\"включен\\\" if monitor_services else \\\"все сервисы\\\"}') + + last_config_update = time.time() + + while True: + try: + # Проверяем нужно ли обновить конфиг (каждые 5 минут) + if time.time() - last_config_update > 300: + print('Проверка обновления конфигурации...') + config = get_config_from_server() + last_config_update = time.time() + + # Обновляем интервал если изменился + interval = config.get('interval_seconds', 60) + monitor_services = config.get('monitor_services', []) + + # Собираем метрики + metrics = get_metrics() + + # Собираем сервисы + services = get_services() + + # Если указаны конкретные сервисы для мониторинга - фильтруем + if monitor_services: + services = [s for s in services if s['name'] in monitor_services] + print(f'Мониторинг {len(services)} сервисов: {[s[\\\"name\\\"] for s in services]}') + + # Отправляем данные + success = send_metrics(config, metrics, services) + + if success: + print(f'Метрики отправлены: CPU={metrics[\\\"cpu_load\\\"]}%, RAM={metrics[\\\"ram_used\\\"]}%, Disk={metrics[\\\"disk_used\\\"]}%') + else: + print('Ошибка отправки метрик') + + # Ждем указанный интервал + time.sleep(interval) + + except KeyboardInterrupt: + print('Агент остановлен') + break + except Exception as e: + print(f'Ошибка: {e}') + time.sleep(10) + +if __name__ == '__main__': + main() +PYTHON_EOF + +# Создаем systemd сервис +cat > /etc/systemd/system/server-monitor-agent.service << 'SERVICE_EOF' +[Unit] +Description=Server Monitor Agent +After=network.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/server-monitor-agent +ExecStart=/usr/bin/python3 /opt/server-monitor-agent/agent.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +SERVICE_EOF + +# Делаем скрипт исполняемым +chmod +x agent.py + +# Перезагружаем systemd +systemctl daemon-reload + +# Включаем автозапуск сервиса +systemctl enable server-monitor-agent + +# Запускаем сервис +systemctl start server-monitor-agent + +echo 'Агент мониторинга установлен и запущен!' +echo 'Статус сервиса:' +systemctl status server-monitor-agent +"; + + $response->getBody()->write($script); + return $response + ->withHeader('Content-Type', 'application/x-shellscript') + ->withHeader('Content-Disposition', 'attachment; filename="install.sh"'); + } + + public function getConfig(Request $request, Response $response, $args) + { + $serverId = $args['id']; + + // Получаем конфигурацию агента + $stmt = $this->pdo->prepare(" + SELECT interval_seconds, monitor_services, enabled + FROM agent_configs + WHERE server_id = :server_id + "); + $stmt->execute([':server_id' => $serverId]); + $config = $stmt->fetch(); + + if (!$config) { + // Если конфигурации нет - создаем с дефолтными значениями + $stmt = $this->pdo->prepare(" + INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled) + VALUES (:server_id, 60, '[]', TRUE) + "); + $stmt->execute([':server_id' => $serverId]); + + $config = [ + 'interval_seconds' => 60, + 'monitor_services' => [], + 'enabled' => true + ]; + } + + $response->getBody()->write(json_encode($config)); + return $response->withHeader('Content-Type', 'application/json'); + } + + public function updateConfig(Request $request, Response $response, $args) + { + $serverId = $args['id']; + $params = $request->getParsedBody(); + + // Получаем и десериализуем массив сервисов + $monitorServices = $params['monitor_services'] ?? []; + + if (is_string($monitorServices)) { + $monitorServices = json_decode($monitorServices, true) ?? []; + } + + // Валидация интервала + $interval = max(10, min(3600, (int)($params['interval_seconds'] ?? 60))); + + // Обновляем конфигурацию + $stmt = $this->pdo->prepare(" + INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled) + VALUES (:server_id, :interval, :services, TRUE) + ON DUPLICATE KEY UPDATE + interval_seconds = VALUES(interval_seconds), + monitor_services = VALUES(monitor_services), + updated_at = CURRENT_TIMESTAMP + "); + + $stmt->execute([ + ':server_id' => $serverId, + ':interval' => $interval, + ':services' => json_encode($monitorServices) + ]); + + // Обновляем статус проверки сервисов на сервере + $enabled = $params['enabled'] ?? true; + $stmt = $this->pdo->prepare(" + UPDATE servers SET service_check_enabled = :enabled WHERE id = :server_id + "); + $stmt->execute([ + ':server_id' => $serverId, + ':enabled' => $enabled ? 1 : 0 + ]); + + return $response->withHeader('Content-Type', 'application/json'); + } + + public function getStatus(Request $request, Response $response, $args) + { + $serverId = $args['id']; + + // Получаем последний раз когда агент был активен + $stmt = $this->pdo->prepare(" + SELECT s.last_metrics_at, s.last_service_check_at, ac.enabled + FROM servers s + LEFT JOIN agent_configs ac ON s.id = ac.server_id + WHERE s.id = :server_id + "); + $stmt->execute([':server_id' => $serverId]); + $result = $stmt->fetch(); + + if (!$result) { + $response->getBody()->write(json_encode(['error' => 'Server not found'])); + return $response->withStatus(404); + } + + $data = [ + 'status' => $result['enabled'] ? 'active' : 'disabled', + 'last_seen' => $result['last_metrics_at'], + 'last_service_check' => $result['last_service_check_at'] + ]; + + $response->getBody()->write(json_encode($data)); + return $response->withHeader('Content-Type', 'application/json'); + } +} \ No newline at end of file diff --git a/src/Controllers/AlertController.php b/src/Controllers/AlertController.php new file mode 100755 index 0000000..c1eb87f --- /dev/null +++ b/src/Controllers/AlertController.php @@ -0,0 +1,60 @@ +twig = $twig; + } + + public function index(Request $request, Response $response, $args) + { + $stmt = $this->pdo->prepare(" + SELECT a.*, s.name as server_name + FROM alerts a + JOIN servers s ON a.server_id = s.id + WHERE a.resolved = 0 + ORDER BY a.created_at DESC + "); + $stmt->execute(); + $alerts = $stmt->fetchAll(); + + $templateData = [ + 'title' => 'Алерты', + 'alerts' => $alerts + ]; + + return $this->twig->render($response, 'alerts/index.twig', $templateData); + } + + public function markAsResolved(Request $request, Response $response, $args) + { + $id = $args['id']; + + $stmt = $this->pdo->prepare(" + UPDATE alerts + SET resolved = 1, resolved_at = NOW() + WHERE id = :id + "); + + $result = $stmt->execute([':id' => $id]); + + if ($result) { + return $response->withHeader('Location', '/alerts')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/alerts')->withStatus(302); + } + } +} \ No newline at end of file diff --git a/src/Controllers/Api/MetricsController.php b/src/Controllers/Api/MetricsController.php new file mode 100755 index 0000000..c99ad57 --- /dev/null +++ b/src/Controllers/Api/MetricsController.php @@ -0,0 +1,229 @@ +getBody(), true); + + if (!$input || !isset($input['token'])) { + return $response->withStatus(400); + } + + $token = $input['token']; + $metrics = $input['metrics'] ?? []; + $services = $input['services'] ?? []; + + // Проверяем токен (сравниваем хеш) + $tokenHash = hash('sha256', $token); + + $stmt = $this->pdo->prepare(" + SELECT at.server_id, s.name as server_name + FROM agent_tokens at + JOIN servers s ON at.server_id = s.id + WHERE at.token_hash = :token_hash + "); + + $stmt->execute([':token_hash' => $tokenHash]); + $tokenInfo = $stmt->fetch(); + + if (!$tokenInfo) { + return $response->withStatus(401); + } + + $serverId = $tokenInfo['server_id']; + + // Обновляем время последних метрик для сервера + $stmt = $this->pdo->prepare(" + UPDATE servers + SET last_metrics_at = NOW() + WHERE id = :server_id + "); + $stmt->execute([':server_id' => $serverId]); + + // Сохраняем метрики если есть + if (!empty($metrics)) { + foreach ($metrics as $metricName => $value) { + // Получаем ID метрики из справочника + $stmt = $this->pdo->prepare("SELECT id FROM metric_names WHERE name = :name"); + $stmt->execute([':name' => $metricName]); + $metricInfo = $stmt->fetch(); + + if ($metricInfo) { + $metricId = $metricInfo['id']; + + // Сохраняем метрику + $stmt = $this->pdo->prepare(" + INSERT INTO server_metrics (server_id, metric_name_id, value) + VALUES (:server_id, :metric_name_id, :value) + "); + $stmt->execute([ + ':server_id' => $serverId, + ':metric_name_id' => $metricId, + ':value' => $value + ]); + + // Проверяем пороги + $this->checkThresholds($serverId, $metricId, $value, $metricName); + } + } + } + + // Сохраняем статусы сервисов если есть + if (!empty($services)) { + foreach ($services as $service) { + $serviceName = $service['name'] ?? ''; + $serviceStatus = $service['status'] ?? 'unknown'; + $loadState = $service['load_state'] ?? ''; + $activeState = $service['active_state'] ?? ''; + $subState = $service['sub_state'] ?? ''; + + if (empty($serviceName)) { + continue; + } + + // Обновляем статус сервиса (INSERT OR UPDATE) + $stmt = $this->pdo->prepare(" + INSERT INTO service_status (server_id, service_name, status, load_state, active_state, sub_state, updated_at) + VALUES (:server_id, :service_name, :status, :load_state, :active_state, :sub_state, NOW()) + ON DUPLICATE KEY UPDATE + status = VALUES(status), + load_state = VALUES(load_state), + active_state = VALUES(active_state), + sub_state = VALUES(sub_state), + updated_at = NOW() + "); + + $stmt->execute([ + ':server_id' => $serverId, + ':service_name' => $serviceName, + ':status' => $serviceStatus, + ':load_state' => $loadState, + ':active_state' => $activeState, + ':sub_state' => $subState + ]); + + // Если сервис остановлен и включено его мониторинг - создаем алерт + if ($serviceStatus === 'stopped') { + $this->createServiceAlert($serverId, $serviceName, $serviceStatus); + } + } + + // Обновляем время последней проверки сервисов + $stmt = $this->pdo->prepare(" + UPDATE servers + SET last_service_check_at = NOW() + WHERE id = :server_id + "); + $stmt->execute([':server_id' => $serverId]); + } + + // Обновляем время последнего использования токена + $stmt = $this->pdo->prepare(" + UPDATE agent_tokens + SET last_used_at = NOW() + WHERE server_id = :server_id + "); + $stmt->execute([':server_id' => $serverId]); + + return $response->withStatus(200); + } + + public function getServices(Request $request, Response $response, $args) + { + $serverId = $args['id']; + + // Получаем список сервисов + $stmt = $this->pdo->prepare(" + SELECT service_name, status, load_state, active_state, sub_state, updated_at + FROM service_status + WHERE server_id = :server_id + ORDER BY service_name + "); + $stmt->execute([':server_id' => $serverId]); + $services = $stmt->fetchAll(); + + $response->getBody()->write(json_encode(['services' => $services])); + return $response->withHeader('Content-Type', 'application/json'); + } + + private function checkThresholds($serverId, $metricId, $value, $metricName) + { + // Получаем пороговые значения для этой метрики на этом сервере + $stmt = $this->pdo->prepare(" + SELECT warning_threshold, critical_threshold + FROM metric_thresholds + WHERE server_id = :server_id AND metric_name_id = :metric_name_id + "); + $stmt->execute([ + ':server_id' => $serverId, + ':metric_name_id' => $metricId + ]); + $thresholds = $stmt->fetch(); + + if ($thresholds) { + $warningThreshold = $thresholds['warning_threshold']; + $criticalThreshold = $thresholds['critical_threshold']; + + $severity = null; + if ($criticalThreshold && $value >= $criticalThreshold) { + $severity = 'critical'; + } elseif ($warningThreshold && $value >= $warningThreshold) { + $severity = 'warning'; + } + + if ($severity) { + // Создаем алерт + $stmt = $this->pdo->prepare(" + INSERT INTO alerts (server_id, metric_name, value, severity) + VALUES (:server_id, :metric_name, :value, :severity) + "); + $stmt->execute([ + ':server_id' => $serverId, + ':metric_name' => $metricName, + ':value' => $value, + ':severity' => $severity + ]); + } + } + } + + private function createServiceAlert($serverId, $serviceName, $status) + { + // Проверяем есть ли уже неразрешенный алерт для этого сервиса + $stmt = $this->pdo->prepare(" + SELECT id FROM service_alerts + WHERE server_id = :server_id AND service_name = :service_name AND resolved = FALSE + ORDER BY created_at DESC LIMIT 1 + "); + + $stmt->execute([ + ':server_id' => $serverId, + ':service_name' => $serviceName + ]); + + $existingAlert = $stmt->fetch(); + + // Если алерта нет или он уже разрешен - создаем новый + if (!$existingAlert) { + $stmt = $this->pdo->prepare(" + INSERT INTO service_alerts (server_id, service_name, status, severity) + VALUES (:server_id, :service_name, :status, 'critical') + "); + + $stmt->execute([ + ':server_id' => $serverId, + ':service_name' => $serviceName, + ':status' => $status + ]); + } + } +} diff --git a/src/Controllers/DashboardController.php b/src/Controllers/DashboardController.php new file mode 100644 index 0000000..88833cb --- /dev/null +++ b/src/Controllers/DashboardController.php @@ -0,0 +1,37 @@ +twig = $twig; + $this->serverModel = new Server(); + } + + public function index(Request $request, Response $response, $args) + { + // Получаем статистику + $stats = $this->serverModel->getStats(); + + // Получаем список серверов с последними метриками + $servers = $this->serverModel->getAll(); + + $templateData = [ + 'title' => 'Дашборд мониторинга', + 'stats' => $stats, + 'servers' => $servers + ]; + + return $this->twig->render($response, 'dashboard.twig', $templateData); + } +} \ No newline at end of file diff --git a/src/Controllers/GroupController.php b/src/Controllers/GroupController.php new file mode 100644 index 0000000..9044471 --- /dev/null +++ b/src/Controllers/GroupController.php @@ -0,0 +1,189 @@ +twig = $twig; + } + + public function index(Request $request, Response $response, $args) + { + $stmt = $this->pdo->prepare("SELECT * FROM server_groups ORDER BY name"); + $stmt->execute(); + $groups = $stmt->fetchAll(); + + $templateData = [ + 'title' => 'Группы серверов', + 'groups' => $groups + ]; + + return $this->twig->render($response, 'groups/index.twig', $templateData); + } + + public function create(Request $request, Response $response, $args) + { + $templateData = [ + 'title' => 'Создать группу' + ]; + + return $this->twig->render($response, 'groups/create.twig', $templateData); + } + + public function store(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + $stmt = $this->pdo->prepare(" + INSERT INTO server_groups (name, description, icon, color) + VALUES (:name, :description, :icon, :color) + "); + + $result = $stmt->execute([ + ':name' => $params['name'], + ':description' => $params['description'] ?? '', + ':icon' => $params['icon'] ?? '', + ':color' => $params['color'] ?? '' + ]); + + if ($result) { + return $response->withHeader('Location', '/groups')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/groups/create')->withStatus(302); + } + } + + public function edit(Request $request, Response $response, $args) + { + $id = $args['id']; + + $stmt = $this->pdo->prepare("SELECT * FROM server_groups WHERE id = :id"); + $stmt->execute([':id' => $id]); + $group = $stmt->fetch(); + + if (!$group) { + return $response->withHeader('Location', '/groups')->withStatus(302); + } + + $templateData = [ + 'title' => 'Редактировать группу', + 'group' => $group + ]; + + return $this->twig->render($response, 'groups/edit.twig', $templateData); + } + + public function update(Request $request, Response $response, $args) + { + $id = $args['id']; + $params = $request->getParsedBody(); + + $stmt = $this->pdo->prepare(" + UPDATE server_groups + SET name = :name, description = :description, icon = :icon, color = :color + WHERE id = :id + "); + + $result = $stmt->execute([ + ':id' => $id, + ':name' => $params['name'], + ':description' => $params['description'] ?? '', + ':icon' => $params['icon'] ?? '', + ':color' => $params['color'] ?? '' + ]); + + if ($result) { + return $response->withHeader('Location', '/groups')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/groups/' . $id . '/edit')->withStatus(302); + } + } + + public function delete(Request $request, Response $response, $args) + { + $id = $args['id']; + + $stmt = $this->pdo->prepare("DELETE FROM server_groups WHERE id = :id"); + $result = $stmt->execute([':id' => $id]); + + if ($result) { + return $response->withHeader('Location', '/groups')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/groups')->withStatus(302); + } + } + + public function show(Request $request, Response $response, $args) + { + $id = $args['id']; + + // Получаем информацию о группе + $stmt = $this->pdo->prepare("SELECT * FROM server_groups WHERE id = :id"); + $stmt->execute([':id' => $id]); + $group = $stmt->fetch(); + + if (!$group) { + return $response->withHeader('Location', '/groups')->withStatus(302); + } + + // Получаем серверы в этой группе + $stmt = $this->pdo->prepare(" + SELECT s.*, + (SELECT created_at FROM server_metrics sm WHERE sm.server_id = s.id ORDER BY sm.created_at DESC LIMIT 1) as last_metrics_at + FROM servers s + WHERE s.group_id = :group_id + ORDER BY s.name + "); + $stmt->execute([':group_id' => $id]); + $servers = $stmt->fetchAll(); + + // Получаем статистику для каждого сервера + $serverStats = []; + foreach ($servers as $server) { + $stmt = $this->pdo->prepare(" + SELECT mn.name, sm.value, mn.unit + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :server_id + ORDER BY sm.created_at DESC + LIMIT 5 + "); + $stmt->execute([':server_id' => $server['id']]); + + $metrics = []; + while ($row = $stmt->fetch()) { + if (!isset($metrics[$row['name']])) { + $metrics[$row['name']] = [ + 'value' => $row['value'], + 'unit' => $row['unit'] + ]; + } + } + + $serverStats[$server['id']] = $metrics; + } + + $templateData = [ + 'title' => 'Группа: ' . $group['name'], + 'group' => $group, + 'servers' => $servers, + 'serverStats' => $serverStats + ]; + + return $this->twig->render($response, 'groups/show.twig', $templateData); + } +} diff --git a/src/Controllers/ServerController.php b/src/Controllers/ServerController.php new file mode 100755 index 0000000..7c916f5 --- /dev/null +++ b/src/Controllers/ServerController.php @@ -0,0 +1,215 @@ +twig = $twig; + } + + public function index(Request $request, Response $response, $args) + { + $stmt = $this->pdo->prepare(" + SELECT s.*, sg.name as group_name + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id + ORDER BY s.name + "); + $stmt->execute(); + $servers = $stmt->fetchAll(); + + $templateData = [ + 'title' => 'Серверы', + 'servers' => $servers + ]; + + return $this->twig->render($response, 'servers/index.twig', $templateData); + } + + public function create(Request $request, Response $response, $args) + { + $stmt = $this->pdo->prepare("SELECT * FROM server_groups ORDER BY name"); + $stmt->execute(); + $groups = $stmt->fetchAll(); + + $templateData = [ + 'title' => 'Добавить сервер', + 'groups' => $groups + ]; + + return $this->twig->render($response, 'servers/create.twig', $templateData); + } + + public function store(Request $request, Response $response, $args) + { + $params = $request->getParsedBody(); + + // Генерируем уникальный токен + $token = bin2hex(random_bytes(16)); // 32-символьный токен + + $this->pdo->beginTransaction(); + + try { + // Сохраняем сервер + $stmt = $this->pdo->prepare(" + INSERT INTO servers (name, address, group_id, description) + VALUES (:name, :address, :group_id, :description) + "); + + $result = $stmt->execute([ + ':name' => $params['name'], + ':address' => $params['address'] ?? '', + ':group_id' => $params['group_id'] ?? null, + ':description' => $params['description'] ?? '' + ]); + + $serverId = $this->pdo->lastInsertId(); + + // Сохраняем хеш токена и зашифрованный токен + $tokenHash = hash('sha256', $token); + $encryptedToken = EncryptionHelper::encrypt($token); + + $stmt = $this->pdo->prepare(" + INSERT INTO agent_tokens (server_id, token_hash, encrypted_token) + VALUES (:server_id, :token_hash, :encrypted_token) + "); + + $result = $stmt->execute([ + ':server_id' => $serverId, + ':token_hash' => $tokenHash, + ':encrypted_token' => $encryptedToken + ]); + + $this->pdo->commit(); + + // Передаем токен для отображения на странице + $templateData = [ + 'title' => 'Сервер добавлен', + 'server' => [ + 'id' => $serverId, + 'name' => $params['name'] + ], + 'token' => $token + ]; + + return $this->twig->render($response, 'servers/created.twig', $templateData); + + } catch (\Exception $e) { + $this->pdo->rollback(); + + // TODO: Обработка ошибки + return $response->withHeader('Location', '/servers/create')->withStatus(302); + } + } + + public function edit(Request $request, Response $response, $args) + { + $id = $args['id']; + + $stmt = $this->pdo->prepare("SELECT * FROM servers WHERE id = :id"); + $stmt->execute([':id' => $id]); + $server = $stmt->fetch(); + + $stmt = $this->pdo->prepare("SELECT * FROM server_groups ORDER BY name"); + $stmt->execute(); + $groups = $stmt->fetchAll(); + + if (!$server) { + return $response->withHeader('Location', '/servers')->withStatus(302); + } + + $templateData = [ + 'title' => 'Редактировать сервер', + 'server' => $server, + 'groups' => $groups + ]; + + return $this->twig->render($response, 'servers/edit.twig', $templateData); + } + + public function update(Request $request, Response $response, $args) + { + $id = $args['id']; + $params = $request->getParsedBody(); + + $stmt = $this->pdo->prepare(" + UPDATE servers + SET name = :name, address = :address, group_id = :group_id, description = :description + WHERE id = :id + "); + + $result = $stmt->execute([ + ':id' => $id, + ':name' => $params['name'], + ':address' => $params['address'] ?? '', + ':group_id' => $params['group_id'] ?? null, + ':description' => $params['description'] ?? '' + ]); + + if ($result) { + return $response->withHeader('Location', '/servers')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302); + } + } + + public function delete(Request $request, Response $response, $args) + { + $id = $args['id']; + + $stmt = $this->pdo->prepare("DELETE FROM servers WHERE id = :id"); + $result = $stmt->execute([':id' => $id]); + + if ($result) { + return $response->withHeader('Location', '/servers')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/servers')->withStatus(302); + } + } + + public function regenerateToken(Request $request, Response $response, $args) + { + $id = $args['id']; + + // Генерируем новый токен + $newToken = bin2hex(random_bytes(16)); + $tokenHash = hash('sha256', $newToken); + $encryptedToken = EncryptionHelper::encrypt($newToken); + + // Обновляем или создаем запись в agent_tokens + $stmt = $this->pdo->prepare(" + INSERT INTO agent_tokens (server_id, token_hash, encrypted_token) + VALUES (:server_id, :token_hash, :encrypted_token) + ON DUPLICATE KEY UPDATE token_hash = :token_hash, encrypted_token = :encrypted_token + "); + + $result = $stmt->execute([ + ':server_id' => $id, + ':token_hash' => $tokenHash, + ':encrypted_token' => $encryptedToken + ]); + + if ($result) { + // Перенаправляем обратно на страницу редактирования + return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302); + } else { + // TODO: Обработка ошибки + return $response->withHeader('Location', '/servers/' . $id . '/edit')->withStatus(302); + } + } +} diff --git a/src/Controllers/ServerDetailController.php b/src/Controllers/ServerDetailController.php new file mode 100755 index 0000000..a101b01 --- /dev/null +++ b/src/Controllers/ServerDetailController.php @@ -0,0 +1,197 @@ +twig = $twig; + } + + public function show(Request $request, Response $response, $args) + { + $id = $args['id']; + + // Получаем информацию о сервере + $stmt = $this->pdo->prepare(" + SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id + WHERE s.id = :id + "); + $stmt->execute([':id' => $id]); + $server = $stmt->fetch(); + + if (!$server) { + return $response->withHeader('Location', '/servers')->withStatus(302); + } + + // Получаем период для выборки метрик + $period = $request->getQueryParams()['period'] ?? '24h'; + + // Определяем интервал времени в зависимости от периода + $interval = match($period) { + '7d' => 'INTERVAL 7 DAY', + '30d' => 'INTERVAL 30 DAY', + default => 'INTERVAL 24 HOUR' + }; + + // Получаем последние метрики для этого сервера + $stmt = $this->pdo->prepare(" + SELECT sm.value, mn.name, mn.unit, sm.created_at + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :id + AND sm.created_at >= DATE_SUB(NOW(), {$interval}) + ORDER BY sm.created_at DESC + "); + $stmt->execute([':id' => $id]); + $metrics = $stmt->fetchAll(); + + // Группируем метрики по типу (CPU, RAM, Disk) + $groupedMetrics = []; + foreach ($metrics as $metric) { + $metricName = $metric['name']; + if (!isset($groupedMetrics[$metricName])) { + $groupedMetrics[$metricName] = []; + } + $groupedMetrics[$metricName][] = $metric; + } + + // Получаем текущие пороговые значения для сервера + $stmt = $this->pdo->prepare(" + SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name + FROM metric_thresholds mt + JOIN metric_names mn ON mt.metric_name_id = mn.id + WHERE mt.server_id = :id + "); + $stmt->execute([':id' => $id]); + $existingThresholds = []; + foreach ($stmt->fetchAll() as $threshold) { + $existingThresholds[$threshold['name']] = [ + 'warning' => $threshold['warning_threshold'], + 'critical' => $threshold['critical_threshold'], + 'duration' => $threshold['duration'] + ]; + } + + // Получаем все типы метрик + $stmt = $this->pdo->query("SELECT id, name, unit FROM metric_names ORDER BY name"); + $allMetricTypes = $stmt->fetchAll(); + + // Получаем список сервисов + $stmt = $this->pdo->prepare(" + SELECT service_name, status, load_state, active_state, sub_state + FROM service_status + WHERE server_id = :server_id + ORDER BY service_name + "); + $stmt->execute([':server_id' => $id]); + $allServices = $stmt->fetchAll(); + + // Получаем список сервисов для мониторинга из конфигурации агента + $stmt = $this->pdo->prepare(" + SELECT monitor_services FROM agent_configs WHERE server_id = :server_id + "); + $stmt->execute([':server_id' => $id]); + $agentConfig = $stmt->fetch(); + + $monitorServices = []; + if ($agentConfig && !empty($agentConfig['monitor_services'])) { + $monitorServices = json_decode($agentConfig['monitor_services'], true) ?? []; + } + + $templateData = [ + 'title' => 'Сервер: ' . $server['name'], + 'server' => $server, + 'metrics' => $groupedMetrics, + 'allMetricTypes' => $allMetricTypes, + 'existingThresholds' => $existingThresholds, + 'allServices' => $allServices, + 'monitorServices' => $monitorServices, + 'period' => $period, + 'request' => $request->getQueryParams() + ]; + + return $this->twig->render($response, 'servers/detail.twig', $templateData); + } + + public function saveThresholds(Request $request, Response $response, $args) + { + $id = $args['id']; + $params = $request->getParsedBody(); + + // Получаем все типы метрик + $stmt = $this->pdo->query("SELECT id, name FROM metric_names ORDER BY name"); + $metricTypes = $stmt->fetchAll(); + + // Удаляем старые пороги для этого сервера + $stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id"); + $stmt->execute([':server_id' => $id]); + + // Добавляем новые пороги + $insertStmt = $this->pdo->prepare(" + INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration) + VALUES (:server_id, :metric_name_id, :warning_threshold, :critical_threshold, :duration) + "); + + foreach ($metricTypes as $metricType) { + $warning = $params[$metricType['name'] . '_warning'] ?? ''; + $critical = $params[$metricType['name'] . '_critical'] ?? ''; + $duration = $params[$metricType['name'] . '_duration'] ?? 0; + + if ($warning !== '' && $critical !== '') { + $insertStmt->execute([ + ':server_id' => $id, + ':metric_name_id' => $metricType['id'], + ':warning_threshold' => $warning, + ':critical_threshold' => $critical, + ':duration' => $duration + ]); + } + } + + // Возвращаемся на страницу сервера + return $response->withHeader('Location', "/servers/{$id}")->withStatus(302); + } + + public function saveServices(Request $request, Response $response, $args) + { + $id = $args['id']; + $params = $request->getParsedBody(); + + // Получаем список сервисов для мониторинга + $services = $params['services'] ?? []; + + if (is_string($services)) { + $services = json_decode($services, true) ?? []; + } + + // Обновляем конфигурацию агента + $stmt = $this->pdo->prepare(" + INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled) + VALUES (:server_id, 60, :services, TRUE) + ON DUPLICATE KEY UPDATE + monitor_services = VALUES(monitor_services), + updated_at = CURRENT_TIMESTAMP + "); + + $stmt->execute([ + ':server_id' => $id, + ':services' => json_encode($services) + ]); + + // Возвращаемся на страницу сервера + return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302); + } +} diff --git a/src/Controllers/ServerDetailController.php.broken b/src/Controllers/ServerDetailController.php.broken new file mode 100755 index 0000000..a3b3278 --- /dev/null +++ b/src/Controllers/ServerDetailController.php.broken @@ -0,0 +1,198 @@ +twig = $twig; + } + + public function show(Request $request, Response $response, $args) + { + $id = $args['id']; + + // Получаем информацию о сервере + $stmt = $this->pdo->prepare(" + SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id + WHERE s.id = :id + "); + $stmt->execute([':id' => $id]); + $server = $stmt->fetch(); + + if (!$server) { + return $response->withHeader('Location', '/servers')->withStatus(302); + } + + // Получаем период для выборки метрик + $period = $request->getQueryParams()['period'] ?? '24h'; + + // Определяем интервал времени в зависимости от периода + $interval = match($period) { + '7d' => 'INTERVAL 7 DAY', + '30d' => 'INTERVAL 30 DAY', + default => 'INTERVAL 24 HOUR' + }; + + // Получаем последние метрики для этого сервера + $stmt = $this->pdo->prepare(" + SELECT sm.value, mn.name, mn.unit, sm.created_at + FROM server_metrics sm + JOIN metric_names mn ON sm.metric_name_id = mn.id + WHERE sm.server_id = :id + AND sm.created_at >= DATE_SUB(NOW(), {$interval}) + ORDER BY sm.created_at DESC + "); + $stmt->execute([':id' => $id]); + $metrics = $stmt->fetchAll(); + + // Группируем метрики по типу + $groupedMetrics = []; + foreach ($metrics as $metric) { + $name = $metric['name']; + if (!isset($groupedMetrics[$name])) { + $groupedMetrics[$name] = []; + } + $groupedMetrics[$name][] = $metric; + } + + // Получаем все доступные типы метрик для настройки порогов + $stmt = $this->pdo->query("SELECT name, unit FROM metric_names ORDER BY name"); + $allMetricTypes = $stmt->fetchAll(); + + // Получаем текущие пороговые значения для сервера + $stmt = $this->pdo->prepare(" + SELECT mt.warning_threshold, mt.critical_threshold, mt.duration, mn.name + FROM metric_thresholds mt + JOIN metric_names mn ON mt.metric_name_id = mn.id + WHERE mt.server_id = :id + "); + $stmt->execute([':id' => $id]); + $existingThresholds = []; + foreach ($stmt->fetchAll() as $threshold) { + $existingThresholds[$threshold['name']] = [ + 'warning' => $threshold['warning_threshold'], + 'critical' => $threshold['critical_threshold'], + 'duration' => $threshold['duration'] + ]; + } + + // Получаем список сервисов + $stmt = $this->pdo->prepare(" + SELECT service_name, status, load_state, active_state, sub_state + FROM service_status + WHERE server_id = :server_id + ORDER BY service_name + "); + $stmt->execute([':server_id' => $id]); + $allServices = $stmt->fetchAll(); + + // Получаем список сервисов для мониторинга из конфигурации агента + $stmt = $this->pdo->prepare(" + SELECT monitor_services FROM agent_configs WHERE server_id = :server_id + "); + $stmt->execute([':server_id' => $id]); + $agentConfig = $stmt->fetch(); + + $monitorServices = []; + if ($agentConfig && !empty($agentConfig['monitor_services'])) { + $monitorServices = json_decode($agentConfig['monitor_services'], true) ?? []; + } + + $templateData = [ + 'title' => 'Сервер: ' . $server['name'], + 'server' => $server, + 'metrics' => $groupedMetrics, + 'allMetricTypes' => $allMetricTypes, + 'existingThresholds' => $existingThresholds, + 'allServices' => $allServices, + 'period' => $period, + 'request' => $request->getQueryParams() + 'monitorServices' => $monitorServices + ]; + + return $this->twig->render($response, 'servers/detail.twig', $templateData); + } + + public function saveThresholds(Request $request, Response $response, $args) + { + $id = $args['id']; + $params = $request->getParsedBody(); + + // Получаем все типы метрик + $stmt = $this->pdo->query("SELECT id, name FROM metric_names ORDER BY name"); + $metricTypes = $stmt->fetchAll(); + + // Удаляем старые пороги для этого сервера + $stmt = $this->pdo->prepare("DELETE FROM metric_thresholds WHERE server_id = :server_id"); + $stmt->execute([':server_id' => $id]); + + // Добавляем новые пороги + $insertStmt = $this->pdo->prepare(" + INSERT INTO metric_thresholds (server_id, metric_name_id, warning_threshold, critical_threshold, duration) + VALUES (:server_id, :metric_name_id, :warning_threshold, :critical_threshold, :duration) + "); + + foreach ($metricTypes as $metricType) { + $warning = $params[$metricType['name'] . '_warning'] ?? null; + $critical = $params[$metricType['name'] . '_critical'] ?? null; + $duration = $params[$metricType['name'] . '_duration'] ?? 0; + + // Сохраняем только если указан хотя бы один порог + if ($warning !== null || $critical !== null) { + $insertStmt->execute([ + ':server_id' => $id, + ':metric_name_id' => $metricType['id'], + ':warning_threshold' => $warning, + ':critical_threshold' => $critical, + ':duration' => (int)$duration + ]); + } + } + + // Возвращаемся на страницу сервера + return $response->withHeader('Location', "/servers/{$id}")->withStatus(302); + } + + public function saveServices(Request $request, Response $response, $args) + { + $id = $args['id']; + $params = $request->getParsedBody(); + + // Получаем список сервисов для мониторинга + $services = $params['services'] ?? []; + + if (is_string($services)) { + $services = json_decode($services, true) ?? []; + } + + // Обновляем конфигурацию агента + $stmt = $this->pdo->prepare(" + INSERT INTO agent_configs (server_id, interval_seconds, monitor_services, enabled) + VALUES (:server_id, 60, :services, TRUE) + ON DUPLICATE KEY UPDATE + monitor_services = VALUES(monitor_services), + updated_at = CURRENT_TIMESTAMP + "); + + $stmt->execute([ + ':server_id' => $id, + ':services' => json_encode($services) + ]); + + // Возвращаемся на страницу сервера + return $response->withHeader('Location', "/servers/{$id}?tab=services")->withStatus(302); + } +} diff --git a/src/Middlewares/AuthMiddleware.php b/src/Middlewares/AuthMiddleware.php new file mode 100755 index 0000000..84714bf --- /dev/null +++ b/src/Middlewares/AuthMiddleware.php @@ -0,0 +1,27 @@ +withHeader('Location', '/login') + ->withStatus(302); + } + + // Если авторизован, продолжаем выполнение + return $handler->handle($request); + } +} \ No newline at end of file diff --git a/src/Middlewares/CsrfMiddleware.php b/src/Middlewares/CsrfMiddleware.php new file mode 100644 index 0000000..867db73 --- /dev/null +++ b/src/Middlewares/CsrfMiddleware.php @@ -0,0 +1,44 @@ +twig = $twig; + $this->csrf = $csrf; + } + + public function __invoke(Request $request, RequestHandler $handler): Response + { + // Пропускаем через handler, чтобы Guard мог установить атрибуты + $response = $handler->handle($request); + + // После обработки получаем токены + $csrfNameKey = $this->csrf->getTokenNameKey(); + $csrfValueKey = $this->csrf->getTokenValueKey(); + + $csrfName = $request->getAttribute($csrfNameKey); + $csrfValue = $request->getAttribute($csrfValueKey); + + $this->twig->getEnvironment()->addGlobal('csrf', [ + 'name_key' => $csrfNameKey, + 'value_key' => $csrfValueKey, + 'name' => $csrfName, + 'value' => $csrfValue + ]); + + return $response; + } +} diff --git a/src/Middlewares/SessionMiddleware.php b/src/Middlewares/SessionMiddleware.php new file mode 100644 index 0000000..4de4317 --- /dev/null +++ b/src/Middlewares/SessionMiddleware.php @@ -0,0 +1,35 @@ +twig = $twig; + } + + public function __invoke(Request $request, RequestHandler $handler): Response + { + // Добавляем данные сессии в контекст Twig + $sessionData = [ + 'user_id' => $_SESSION['user_id'] ?? null, + 'username' => $_SESSION['username'] ?? null, + 'role' => $_SESSION['role'] ?? null + ]; + + // Получаем environment и добавляем session в глобальный контекст + $environment = $this->twig->getEnvironment(); + $environment->addGlobal('session', $sessionData); + + return $handler->handle($request); + } +} \ No newline at end of file diff --git a/src/Models/Alert.php b/src/Models/Alert.php new file mode 100644 index 0000000..f01b876 --- /dev/null +++ b/src/Models/Alert.php @@ -0,0 +1,46 @@ +db = DatabaseConfig::getInstance(); + } + + public function getAll($resolved = false) + { + $stmt = $this->db->prepare("SELECT a.*, s.name as server_name, sg.name as group_name, sg.icon as group_icon + FROM alerts a + JOIN servers s ON a.server_id = s.id + LEFT JOIN server_groups sg ON s.group_id = sg.id + WHERE a.resolved = ? + ORDER BY a.created_at DESC"); + $stmt->execute([$resolved ? 1 : 0]); + return $stmt->fetchAll(); + } + + public function getById($id) + { + $stmt = $this->db->prepare("SELECT a.*, s.name as server_name, sg.name as group_name, sg.icon as group_icon + FROM alerts a + JOIN servers s ON a.server_id = s.id + LEFT JOIN server_groups sg ON s.group_id = sg.id + WHERE a.id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); + } + + public function markAsResolved($id) + { + $stmt = $this->db->prepare("UPDATE alerts SET resolved = TRUE, resolved_at = NOW() WHERE id = ?"); + return $stmt->execute([$id]); + } +} \ No newline at end of file diff --git a/src/Models/Group.php b/src/Models/Group.php new file mode 100644 index 0000000..6739d20 --- /dev/null +++ b/src/Models/Group.php @@ -0,0 +1,62 @@ +db = DatabaseConfig::getInstance(); + } + + public function getAll() + { + $stmt = $this->db->query("SELECT * FROM server_groups ORDER BY name"); + return $stmt->fetchAll(); + } + + public function getById($id) + { + $stmt = $this->db->prepare("SELECT * FROM server_groups WHERE id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); + } + + public function create($data) + { + $stmt = $this->db->prepare("INSERT INTO server_groups (name, description, icon, color) + VALUES (?, ?, ?, ?)"); + return $stmt->execute([ + $data['name'], + $data['description'] ?? null, + $data['icon'] ?? null, + $data['color'] ?? null + ]); + } + + public function update($id, $data) + { + $stmt = $this->db->prepare("UPDATE server_groups + SET name = ?, description = ?, icon = ?, color = ? + WHERE id = ?"); + return $stmt->execute([ + $data['name'], + $data['description'] ?? null, + $data['icon'] ?? null, + $data['color'] ?? null, + $id + ]); + } + + public function delete($id) + { + $stmt = $this->db->prepare("DELETE FROM server_groups WHERE id = ?"); + return $stmt->execute([$id]); + } +} \ No newline at end of file diff --git a/src/Models/Model.php b/src/Models/Model.php new file mode 100755 index 0000000..e8cb0e8 --- /dev/null +++ b/src/Models/Model.php @@ -0,0 +1,27 @@ +pdo = DatabaseConfig::getInstance(); + } + + /** + * Экранирование данных для безопасности + */ + protected function sanitize($data) + { + if (is_array($data)) { + return array_map('htmlspecialchars', $data); + } + return htmlspecialchars($data, ENT_QUOTES, 'UTF-8'); + } +} \ No newline at end of file diff --git a/src/Models/Server.php b/src/Models/Server.php new file mode 100644 index 0000000..a926c22 --- /dev/null +++ b/src/Models/Server.php @@ -0,0 +1,59 @@ +db = DatabaseConfig::getInstance(); + } + + public function getAll() + { + $stmt = $this->db->query("SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id + ORDER BY s.name"); + return $stmt->fetchAll(); + } + + public function getById($id) + { + $stmt = $this->db->prepare("SELECT s.*, sg.name as group_name, sg.icon as group_icon, sg.color as group_color + FROM servers s + LEFT JOIN server_groups sg ON s.group_id = sg.id + WHERE s.id = ?"); + $stmt->execute([$id]); + return $stmt->fetch(); + } + + public function getStats() + { + $stats = []; + + // Общее количество серверов + $stmt = $this->db->query("SELECT COUNT(*) as total FROM servers"); + $stats['total_servers'] = $stmt->fetch()['total']; + + // Количество групп + $stmt = $this->db->query("SELECT COUNT(*) as total FROM server_groups"); + $stats['total_groups'] = $stmt->fetch()['total']; + + // Активные алерты (warning) + $stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'warning'"); + $stats['warnings'] = $stmt->fetch()['total']; + + // Активные алерты (critical) + $stmt = $this->db->query("SELECT COUNT(*) as total FROM alerts WHERE resolved = FALSE AND severity = 'critical'"); + $stats['criticals'] = $stmt->fetch()['total']; + + return $stats; + } +} \ No newline at end of file diff --git a/src/Models/User.php b/src/Models/User.php new file mode 100755 index 0000000..fdc60bf --- /dev/null +++ b/src/Models/User.php @@ -0,0 +1,42 @@ +pdo->prepare("SELECT * FROM users WHERE username = :username LIMIT 1"); + $stmt->execute([':username' => $username]); + return $stmt->fetch(); + } + + public function authenticate($username, $password) + { + $user = $this->findByUsername($username); + + if ($user && password_verify($password, $user['password_hash'])) { + return $user; + } + + return false; + } + + public function create($username, $password, $email, $role = 'user') + { + $passwordHash = password_hash($password, PASSWORD_DEFAULT); + + $stmt = $this->pdo->prepare(" + INSERT INTO users (username, password_hash, email, role) + VALUES (:username, :password_hash, :email, :role) + "); + + return $stmt->execute([ + ':username' => $username, + ':password_hash' => $passwordHash, + ':email' => $email, + ':role' => $role + ]); + } +} \ No newline at end of file diff --git a/src/Utils/EncryptionHelper.php b/src/Utils/EncryptionHelper.php new file mode 100644 index 0000000..ac6f100 --- /dev/null +++ b/src/Utils/EncryptionHelper.php @@ -0,0 +1,37 @@ + +
+
+
+

Настройки уведомлений

+
+
+
+
Email уведомления
+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ +
+
Telegram уведомления
+
+
+ + +
+
+ + +
+
+
+ +
+
SMS уведомления
+
+
+ + +
+
+ + +
+
+
+ +
+ +
+ +
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/users.twig b/templates/admin/users.twig new file mode 100755 index 0000000..4d51c6a --- /dev/null +++ b/templates/admin/users.twig @@ -0,0 +1,70 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+

Управление пользователями

+ + Добавить пользователя + +
+ +
+
+ {% if users|length > 0 %} +
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
IDИмя пользователяEmailРольДата созданияДействия
{{ user.id }}{{ user.username }}{{ user.email|default('-') }} + {% if user.role == 'admin' %} + Администратор + {% else %} + Пользователь + {% endif %} + {{ user.created_at|date('d.m.Y H:i:s') }} + + Редактировать + +
+ +
+
+
+ {% else %} +
+ +

Пользователи пока не созданы

+ + Создать первого пользователя + +
+ {% endif %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/alerts/index.twig b/templates/alerts/index.twig new file mode 100755 index 0000000..1c904d4 --- /dev/null +++ b/templates/alerts/index.twig @@ -0,0 +1,60 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+

Алерты

+
+ +
+
+ {% if alerts|length > 0 %} +
+ + + + + + + + + + + + + {% for alert in alerts %} + + + + + + + + + {% endfor %} + +
СерверМетрикаЗначениеУровеньВремяДействия
{{ alert.server_name }}{{ alert.metric_name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }}{{ alert.value }}{% if alert.metric_name ends_with '_load' or alert.metric_name ends_with '_used' %}%{% endif %} + {% if alert.severity == 'critical' %} + Критично + {% else %} + Предупреждение + {% endif %} + {{ alert.created_at|date('d.m.Y H:i:s') }} + + Исправлено + +
+
+ {% else %} +
+ +

Нет активных алертов

+

Все серверы работают в штатном режиме

+
+ {% endif %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/dashboard.twig b/templates/dashboard.twig new file mode 100755 index 0000000..c83fb53 --- /dev/null +++ b/templates/dashboard.twig @@ -0,0 +1,125 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+

Дашборд мониторинга

+
+
+ + +
+
+
+
+ +

{{ stats.total_servers }}

+

Всего серверов

+
+
+
+
+
+
+ +

{{ stats.servers_with_metrics }}

+

С метриками

+
+
+
+
+
+
+ +

{{ stats.alerts_count }}

+

Активных алертов

+
+
+
+
+ + +
+
+
+ +
+ {% if servers|length > 0 %} +
+ + + + + + + + + + + + + {% for server in servers %} + + + + + + + + + {% endfor %} + +
НазваниеАдресГруппаСтатусПоследние метрикиДействия
{{ server.name }}{{ server.address|default('-') }} + {% if server.group_name %} + + {{ server.group_name }} + + {% else %} + Без группы + {% endif %} + + {% if server.last_metrics_at %} + Активен + {% else %} + Нет метрик + {% endif %} + + {% if server.last_metrics_at %} + {{ server.last_metrics_at|date('d.m.Y H:i:s') }} + {% else %} + - + {% endif %} + + + Просмотр + + + Редактировать + +
+
+ {% else %} +
+ +

Серверы пока не добавлены

+ + Добавить первый сервер + +
+ {% endif %} +
+
+
+
+ +{% endblock %} diff --git a/templates/groups/create.twig b/templates/groups/create.twig new file mode 100755 index 0000000..e01eb83 --- /dev/null +++ b/templates/groups/create.twig @@ -0,0 +1,53 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

Создать группу серверов

+
+
+
+
+ + +
+ +
+ + +
+ +
+
+
+ + + Используйте классы Font Awesome, например: fa-server, fa-desktop +
+
+ +
+
+ + + Выберите цвет для обозначения группы +
+
+
+ +
+ + Назад + + +
+
+
+
+
+
+{% endblock %} diff --git a/templates/groups/edit.twig b/templates/groups/edit.twig new file mode 100755 index 0000000..5f6cbf5 --- /dev/null +++ b/templates/groups/edit.twig @@ -0,0 +1,54 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

Редактировать группу "{{ group.name }}"

+
+
+
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + + Используйте классы Font Awesome, например: fa-server, fa-desktop +
+
+ +
+
+ + + Выберите цвет для обозначения группы +
+
+
+ +
+ + Назад + + +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/groups/index.twig b/templates/groups/index.twig new file mode 100755 index 0000000..62a93d6 --- /dev/null +++ b/templates/groups/index.twig @@ -0,0 +1,74 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+

Группы серверов

+ + Создать группу + +
+ +
+
+ {% if groups|length > 0 %} +
+ + + + + + + + + + + {% for group in groups %} + + + + + + + {% endfor %} + +
НазваниеОписаниеЦветДействия
+ {{ group.name }} + {{ group.description|default('') }} + {% if group.color %} + {{ group.color }} + {% else %} + - + {% endif %} + + + Просмотр + + + Редактировать + +
+ + + + +
+
+
+ {% else %} +
+ +

Группы серверов пока не созданы

+ + Создать первую группу + +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/templates/groups/show.twig b/templates/groups/show.twig new file mode 100644 index 0000000..1c5966c --- /dev/null +++ b/templates/groups/show.twig @@ -0,0 +1,97 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+

+ {{ group.name }} + {% if group.description %}- {{ group.description }}{% endif %} +

+ +
+ +
+
+

Информация о группе

+

Описание: {{ group.description|default('Нет описания') }}

+

Цвет: + {% if group.color %} + {{ group.color }} + {% else %} + Не указан + {% endif %} +

+

Серверов в группе: {{ servers|length }}

+
+
+ +
+
+

Серверы в группе

+
+
+ {% if servers|length > 0 %} +
+ + + + + + + + + + + + {% for server in servers %} + + + + + + + + {% endfor %} + +
НазваниеАдресПоследние метрикиСтатусДействия
+ {{ server.name }} + {{ server.address|default('-') }} + {% if server.last_metrics_at %} + {{ server.last_metrics_at|date('d.m.Y H:i:s') }} + {% else %} + Нет данных + {% endif %} + + {% if server.last_metrics_at %} + Активен + {% else %} + Нет метрик + {% endif %} + + + Просмотр + +
+
+ {% else %} +
+ +

В этой группе пока нет серверов

+ + Добавить сервер + +
+ {% endif %} +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/layout.twig b/templates/layout.twig new file mode 100644 index 0000000..57e5062 --- /dev/null +++ b/templates/layout.twig @@ -0,0 +1,119 @@ + + + + + + {{ title }} - Система мониторинга + + + + + + + + +
+ {% if session.username %} + + {% endif %} + +
+ {% block content %}{% endblock %} +
+ + {% if session.username %} +
+
+ © {{ 'now'|date('Y') }} Система мониторинга серверов +
+
+ {% endif %} +
+ + + + + + + + \ No newline at end of file diff --git a/templates/login-layout.twig b/templates/login-layout.twig new file mode 100644 index 0000000..d68f29a --- /dev/null +++ b/templates/login-layout.twig @@ -0,0 +1,50 @@ + + + + + + {{ title }} - Система мониторинга + + + + + + +
+ {% block content %}{% endblock %} +
+ + + + + + + + diff --git a/templates/login.twig b/templates/login.twig new file mode 100644 index 0000000..4a0ac30 --- /dev/null +++ b/templates/login.twig @@ -0,0 +1,33 @@ +{% extends "login-layout.twig" %} + +{% block content %} +
+
+
+
+

Вход в систему

+
+
+
+ + + +
+ + +
+
+ + +
+
+ +
+
+
+
+
+
+{% endblock %} diff --git a/templates/servers/create.twig b/templates/servers/create.twig new file mode 100755 index 0000000..865791c --- /dev/null +++ b/templates/servers/create.twig @@ -0,0 +1,54 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

Добавить сервер

+
+
+
+
+ + + Укажите понятное название сервера +
+ +
+ + + IP-адрес или доменное имя сервера (не обязательно) +
+ +
+ + + Выберите группу для объединения серверов +
+ +
+ + + Дополнительная информация о сервере +
+ +
+ + Назад + + +
+
+
+
+
+
+{% endblock %} diff --git a/templates/servers/created.twig b/templates/servers/created.twig new file mode 100755 index 0000000..26111ae --- /dev/null +++ b/templates/servers/created.twig @@ -0,0 +1,65 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

Сервер добавлен

+
+
+ + +
+
Инструкция по установке агента:
+
    +
  1. Скачайте скрипт установки с помощью кнопки выше
  2. +
  3. Загрузите его на сервер, который вы хотите мониторить
  4. +
  5. Выполните команду: chmod +x install.sh && ./install.sh
  6. +
  7. Агент начнет отправлять метрики на сервер мониторинга
  8. +
+
+ + +
+
+
+
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/servers/detail.twig b/templates/servers/detail.twig new file mode 100755 index 0000000..669d210 --- /dev/null +++ b/templates/servers/detail.twig @@ -0,0 +1,456 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

+ + {{ server.name }} + {% if server.group_name %} + + {{ server.group_name }} + + {% endif %} +

+ +
+
+ +
+
+
Информация о сервере
+ + + + + + + + + + + + + + + + + + + + + +
Название:{{ server.name }}
Адрес:{{ server.address|default('-') }}
Группа: + {% if server.group_name %} + {{ server.group_name }} + {% else %} + - + {% endif %} +
Описание:{{ server.description|default('-') }}
Последние метрики: + {% if server.last_metrics_at %} + {{ server.last_metrics_at|date('d.m.Y H:i:s') }} + {% else %} + Нет данных + {% endif %} +
+
+
+ + + + + +
+ +
+ + +
+ {% for metricName, metricData in metrics %} +
+
+
+
+ {{ metricName|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }} + {% if metricData[0].unit %}({{ metricData[0].unit }}){% endif %} +
+
+
+ {% if metricData %} +

{{ metricData[0].value }}{{ metricData[0].unit|default('') }}

+

+ {{ metricData[0].created_at|date('d.m.Y H:i:s') }} +

+ + +
+ +
+ {% else %} +

Нет данных за этот период

+ {% endif %} +
+
+
+ {% endfor %} + + {% if metrics|length == 0 %} +
+
+ Нет данных о метриках за выбранный период +
+
+ {% endif %} +
+
+ + +
+
+
+
+
+

+ Сервисы сервера + {% if allServices is defined %} + (найдено: {{ allServices|length }}) + {% endif %} +

+
+ +
+
+
+ +
+
+
+
+
+
+ + +
+ +
+ +
+ {% if allServices is defined and allServices|length > 0 %} + {# Сортируем сервисы: сначала running, потом stopped, потом unknown, затем по имени #} + {% set runningServices = allServices|filter(s => s.status == 'running')|sort((a, b) => a.service_name|lower > b.service_name|lower) %} + {% set stoppedServices = allServices|filter(s => s.status == 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %} + {% set unknownServices = allServices|filter(s => s.status != 'running' and s.status != 'stopped')|sort((a, b) => a.service_name|lower > b.service_name|lower) %} + + {# Выводим все сервисы по порядку #} + {% for service in runningServices %} +
+
+
+
+
+ + +
+
+ running +
+
+
+ Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }} +
+
+
+
+ {% endfor %} + + {% for service in stoppedServices %} +
+
+
+
+
+ + +
+
+ stopped +
+
+
+ Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }} +
+
+
+
+ {% endfor %} + + {% for service in unknownServices %} +
+
+
+
+
+ + +
+
+ unknown +
+
+
+ Load: {{ service.load_state|default('-') }} | Active: {{ service.active_state|default('-') }} +
+
+
+
+ {% endfor %} + {% else %} +
+
+ Агент не отправил список сервисов или не установлен +
+
+
+ +
+ {% endif %} +
+
+
+ +
+ +
+
+
+
+
+ + +
+

Настройка порогов

+
+
+
+ {% for metricType in allMetricTypes %} +
+
+
+ {{ metricType.name|replace({'_': ' ', 'load': 'загрузка', 'used': 'использование'})|title }} + {% if metricType.unit %}({{ metricType.unit }}){% endif %} +
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + + + 0 = алерт сразу при превышении, >0 = алерт только если превышено дольше указанного времени + +
+
+
+
+ {% endfor %} + +
+ +
+
+
+
+
+
+
+
+
+
+ + + + +{% endblock %} diff --git a/templates/servers/edit.twig b/templates/servers/edit.twig new file mode 100755 index 0000000..1ba3202 --- /dev/null +++ b/templates/servers/edit.twig @@ -0,0 +1,74 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

Редактировать сервер "{{ server.name }}"

+
+
+
+ +
+ + + Укажите понятное название сервера +
+ +
+ + + IP-адрес или доменное имя сервера (не обязательно) +
+ +
+ + + Выберите группу для объединения серверов +
+ +
+ + + Дополнительная информация о сервере +
+ +
+ + Назад + + +
+
+ +
+ +
+
Управление агентом мониторинга:
+ +

Если вы потеряли доступ к агенту или хотите создать новый токен безопасности, используйте кнопку "Сбросить токен".

+
+
+
+
+
+{% endblock %} diff --git a/templates/servers/index.twig b/templates/servers/index.twig new file mode 100755 index 0000000..9920edb --- /dev/null +++ b/templates/servers/index.twig @@ -0,0 +1,82 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+ + +
+
+ {% if servers|length > 0 %} +
+ + + + + + + + + + + + + {% for server in servers %} + + + + + + + + + {% endfor %} + +
НазваниеАдресГруппаОписаниеПоследние метрикиДействия
{{ server.name }}{{ server.address|default('-') }} + {% if server.group_name %} + {{ server.group_name }} + {% else %} + - + {% endif %} + {{ server.description|default('-')|slice(0, 30) ~ (attribute(server, 'description')|length > 30 ? '...' : '') }} + {% if server.last_metrics_at %} + {{ server.last_metrics_at|date('d.m.Y H:i:s') }} + {% else %} + Нет данных + {% endif %} + + + Просмотр + + + Редактировать + +
+ + + + +
+
+
+ {% else %} +
+ +

Серверы пока не добавлены

+ + Добавить первый сервер + +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/templates/test.twig b/templates/test.twig new file mode 100755 index 0000000..af1ad44 --- /dev/null +++ b/templates/test.twig @@ -0,0 +1,19 @@ +{% extends "layout.twig" %} + +{% block content %} +
+
+
+
+

{{ title }}

+
+
+

{{ message }}

+ + На главную + +
+
+
+
+{% endblock %} \ No newline at end of file