Итерация 1: каркас Slim 4 приложения
- Slim 4 + php-di контейнер с маршрутизацией - Docker Compose: app (PHP 8.3-FPM) + web (nginx) + db (MariaDB) - Phinx миграция: таблица users (uuid, username, password_hash) - Auth: login/logout с сессиями - Setup: создание первого пользователя - Dashboard с layout (Bootstrap 5.3) - CLI: bin/console, bin/run-scan-worker.php - Smoke test: scripts/check.sh - README.md, PLAN.md
This commit is contained in:
commit
e7732f5cee
|
|
@ -0,0 +1,17 @@
|
|||
APP_ENV=development
|
||||
APP_SECRET=change-this-to-random-string
|
||||
ENCRYPTION_KEY=change-this-to-32-byte-hex
|
||||
|
||||
DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=domovoy
|
||||
DB_USERNAME=domovoy
|
||||
DB_PASSWORD=domovoy
|
||||
|
||||
MYSQL_ROOT_PASSWORD=rootpass
|
||||
|
||||
SSH_CONNECT_TIMEOUT_SECONDS=5
|
||||
SSH_AUTH_TIMEOUT_SECONDS=10
|
||||
SSH_COMMAND_TIMEOUT_SECONDS=8
|
||||
SSH_TOTAL_SCAN_TIMEOUT_SECONDS=60
|
||||
SSH_RETRY_COUNT=0
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
/vendor/
|
||||
.env
|
||||
.phpunit.result.cache
|
||||
|
|
@ -0,0 +1,393 @@
|
|||
# План реализации проекта "Домовой"
|
||||
|
||||
Документ представляет собой пошаговый план разработки.
|
||||
Каждая итерация — самостоятельный кусок функционала, который
|
||||
можно реализовать, протестировать и запустить независимо.
|
||||
|
||||
После каждой итерации запускается scripts/check.sh.
|
||||
Без успешного smoke test итерация не считается завершённой.
|
||||
|
||||
---
|
||||
|
||||
## Итерация 1. Каркас приложения
|
||||
|
||||
Цель: приложение открывается, есть логин, база, миграции, layout.
|
||||
|
||||
Что делаем:
|
||||
- Slim 4 приложение с маршрутами
|
||||
- Docker Compose: app (PHP 8.3) + MariaDB
|
||||
- .env.example со всеми переменными
|
||||
- Phinx: конфигурация, миграция users
|
||||
- Регистрация первого пользователя (CLI setup)
|
||||
- Логин/Logout с сессиями
|
||||
- Bootstrap layout с левым меню и верхней панелью
|
||||
- Dashboard с заглушками счётчиков
|
||||
- scripts/check.sh с базовыми проверками
|
||||
|
||||
Файлы:
|
||||
composer.json
|
||||
phinx.php
|
||||
docker-compose.yml
|
||||
docker/Dockerfile
|
||||
.env.example
|
||||
public/index.php
|
||||
public/assets/css/app.css
|
||||
app/Controllers/AuthController.php
|
||||
app/Controllers/DashboardController.php
|
||||
app/Controllers/SetupController.php
|
||||
app/Middleware/AuthMiddleware.php
|
||||
app/Repositories/UserRepository.php
|
||||
app/Services/AuthService.php
|
||||
templates/layout.php
|
||||
templates/auth/login.php
|
||||
templates/dashboard/index.php
|
||||
migrations/20250526000001CreateUsers.php
|
||||
bin/console
|
||||
scripts/check.sh
|
||||
README.md
|
||||
|
||||
Проверка:
|
||||
docker compose up -d --build
|
||||
docker compose exec app php vendor/bin/phinx migrate
|
||||
docker compose exec app php bin/console setup:user
|
||||
curl -I http://localhost:8080/login
|
||||
# Открыть в браузере, залогиниться, увидеть dashboard
|
||||
|
||||
---
|
||||
|
||||
## Итерация 2. Сканирование сети
|
||||
|
||||
Цель: добавить диапазон, запустить scan, увидеть найденные хосты.
|
||||
|
||||
Что делаем:
|
||||
- Миграции: network_ranges, scan_jobs, discovered_hosts
|
||||
- NetworkRanges CRUD (вкладка Discovery)
|
||||
- NetworkScanner: ping sweep + TCP connect + ARP + reverse DNS
|
||||
- CLI worker bin/run-scan-worker.php
|
||||
- Страница Discovery: таблица discovered_hosts
|
||||
- htmx для запуска scan и обновления статуса scan_jobs
|
||||
- Audit log для действий сканрования
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/20250526000002CreateNetworkRanges.php
|
||||
migrations/20250526000003CreateScanJobs.php
|
||||
migrations/20250526000004CreateDiscoveredHosts.php
|
||||
migrations/20250526000005CreateAuditLog.php
|
||||
app/Controllers/DiscoveryController.php
|
||||
app/Controllers/NetworkRangeController.php
|
||||
app/Services/Discovery/NetworkScanner.php
|
||||
app/Services/Discovery/PingScanner.php
|
||||
app/Services/Discovery/TcpPortScanner.php
|
||||
app/Services/Discovery/ArpTableReader.php
|
||||
app/Services/Discovery/HostFingerprintService.php
|
||||
app/Services/Jobs/ScanJobRunner.php
|
||||
app/Services/Jobs/JobQueue.php
|
||||
app/Repositories/ScanJobRepository.php
|
||||
app/Repositories/DiscoveredHostRepository.php
|
||||
app/Repositories/NetworkRangeRepository.php
|
||||
app/Repositories/AuditLogRepository.php
|
||||
templates/discovery/index.php
|
||||
templates/discovery/ranges.php
|
||||
templates/discovery/hosts.php
|
||||
bin/run-scan-worker.php
|
||||
|
||||
Проверка:
|
||||
# Добавить диапазон 192.168.1.0/24
|
||||
# Запустить scan через UI
|
||||
# В другом терминале:
|
||||
docker compose exec app php bin/run-scan-worker.php
|
||||
# Увидеть найденные хосты в таблице Discovery
|
||||
|
||||
---
|
||||
|
||||
## Итерация 3. Инвентарь устройств
|
||||
|
||||
Цель: создавать карточки устройств руками и из найденных хостов.
|
||||
|
||||
Что делаем:
|
||||
- Миграция devices
|
||||
- Devices CRUD (список, создание, редактирование, карточка)
|
||||
- Создание device из discovered_host (кнопка "Создать устройство")
|
||||
- Игнорирование discovered_host (кнопка "Игнорировать")
|
||||
- Карточка устройства со всеми полями
|
||||
- Merge suggestions (по MAC → высокая, hostname → средняя, IP → низкая)
|
||||
- Обновление Dashboard: реальные счётчики устройств
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateDevices.php
|
||||
app/Controllers/DeviceController.php
|
||||
app/Services/Inventory/DeviceService.php
|
||||
app/Services/Inventory/MergeSuggestionService.php
|
||||
app/Repositories/DeviceRepository.php
|
||||
templates/devices/index.php
|
||||
templates/devices/create.php
|
||||
templates/devices/edit.php
|
||||
templates/devices/show.php
|
||||
templates/dashboard/index.php (обновить счётчики)
|
||||
|
||||
Проверка:
|
||||
# Создать устройство вручную
|
||||
# Создать устройство из discovered_host
|
||||
# Открыть карточку, проверить все поля
|
||||
# Проверить merge suggestions
|
||||
|
||||
---
|
||||
|
||||
## Итерация 4. SSH-доступы
|
||||
|
||||
Цель: добавить SSH-доступ к устройству, проверить подключение.
|
||||
|
||||
Что делаем:
|
||||
- Миграция credentials
|
||||
- Credentials CRUD (только SSH для MVP)
|
||||
- Шифрование секретов (defuse/php-encryption)
|
||||
- Тест подключения через phpseclib
|
||||
- Блок доступов на карточке устройства
|
||||
- CredentialVault для шифрования/дешифрования
|
||||
- Сохранение результата теста (last_test_status, last_test_at)
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateCredentials.php
|
||||
app/Controllers/CredentialController.php
|
||||
app/Services/Security/CredentialVault.php
|
||||
app/Services/HostScan/SshClientFactory.php
|
||||
app/Repositories/CredentialRepository.php
|
||||
templates/credentials/_form.php
|
||||
templates/credentials/_test_result.php
|
||||
templates/devices/show.php (добавить блок доступов)
|
||||
.env.example (добавить ENCRYPTION_KEY)
|
||||
|
||||
Проверка:
|
||||
# Добавить SSH-доступ к устройству
|
||||
# Нажать "Тест" — увидеть результат подключения
|
||||
# Секрет не отображается в UI открытым текстом
|
||||
# В базе хранится зашифрованное значение
|
||||
|
||||
---
|
||||
|
||||
## Итерация 5. Deep scan Linux-хоста
|
||||
|
||||
Цель: по SSH собрать read-only информацию с Linux-хоста.
|
||||
|
||||
Что делаем:
|
||||
- Миграции: host_scans
|
||||
- LinuxHostScanner: hostname, os, ip, ports, disk, systemd, cron
|
||||
- CommandWhitelist с массивами аргументов
|
||||
- SshClientFactory с таймаутами (connect=5s, auth=10s, cmd=8s)
|
||||
- Сохранение raw JSON в storage/scans/
|
||||
- Summary в host_scans.summary_json
|
||||
- Кнопка "Глубокий скан" на карточке устройства
|
||||
- Страница результата скана
|
||||
- Worker поддерживает тип host_deep_scan
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateHostScans.php
|
||||
app/Controllers/HostScanController.php
|
||||
app/Services/HostScan/LinuxHostScanner.php
|
||||
app/Services/HostScan/CommandWhitelist.php
|
||||
app/Repositories/HostScanRepository.php
|
||||
templates/host_scans/show.php
|
||||
templates/devices/show.php (кнопка deep scan)
|
||||
bin/run-scan-worker.php (поддержка host_deep_scan)
|
||||
config/scan_commands.php (CommandWhitelist mapping)
|
||||
.env.example (добавить SSH_* таймауты)
|
||||
|
||||
Проверка:
|
||||
# Добавить SSH-доступ к устройству
|
||||
# Нажать "Глубокий скан"
|
||||
# В другом терминале:
|
||||
docker compose exec app php bin/run-scan-worker.php
|
||||
# Увидеть результат: hostname, os, порты, диски, systemd, cron
|
||||
# raw JSON сохранён в storage/scans/
|
||||
|
||||
---
|
||||
|
||||
## Итерация 6. Docker scan + detected_services
|
||||
|
||||
Цель: найти контейнеры и создать предложения сервисов.
|
||||
|
||||
Что делаем:
|
||||
- Миграция detected_services
|
||||
- DockerScanner: docker ps, inspect, volumes, networks
|
||||
- SecretMasker для env-переменных (PASSWORD, TOKEN, KEY...)
|
||||
- Создание detected_services kind=docker_container
|
||||
- Страница "Найденные сервисы" (detected services)
|
||||
- Действия: Создать сервис, Объединить, Игнорировать, Детали
|
||||
- Host scan обогащается docker-сканом
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateDetectedServices.php
|
||||
app/Controllers/DetectedServiceController.php
|
||||
app/Services/HostScan/DockerScanner.php
|
||||
app/Services/Security/SecretMasker.php
|
||||
app/Repositories/DetectedServiceRepository.php
|
||||
templates/services/detected.php
|
||||
templates/services/_detected_row.php
|
||||
app/Services/HostScan/LinuxHostScanner.php (добавить docker)
|
||||
|
||||
Проверка:
|
||||
# Запустить deep scan на хосте с Docker
|
||||
# Увидеть detected_services с kind=docker_container
|
||||
# Секретные env-переменные замаскированы
|
||||
# Страница найденных сервисов показывает таблицу
|
||||
|
||||
---
|
||||
|
||||
## Итерация 7. Создание сервисов + связи
|
||||
|
||||
Цель: превратить найденный сервис в карточку с отношениями.
|
||||
|
||||
Что делаем:
|
||||
- Миграции: services, service_endpoints, relations
|
||||
- Service CRUD
|
||||
- Создание service из detected_service (с выбором типа)
|
||||
- Автоматическое создание service_endpoints (port, url)
|
||||
- Создание связей (Device runs_on Service, Service exposes Endpoint)
|
||||
- Карточка сервиса
|
||||
- Обновление detected_service.status = accepted
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateServices.php
|
||||
migrations/...CreateServiceEndpoints.php
|
||||
migrations/...CreateRelations.php
|
||||
app/Controllers/ServiceController.php
|
||||
app/Services/Inventory/ServiceInventoryService.php
|
||||
app/Services/Inventory/RelationService.php
|
||||
app/Repositories/ServiceRepository.php
|
||||
app/Repositories/ServiceEndpointRepository.php
|
||||
app/Repositories/RelationRepository.php
|
||||
templates/services/index.php
|
||||
templates/services/create.php
|
||||
templates/services/show.php
|
||||
templates/services/detected.php (кнопка "Создать сервис")
|
||||
|
||||
Проверка:
|
||||
# Нажать "Создать сервис" на detected_service
|
||||
# Выбрать тип (web_app, database...)
|
||||
# Сервис создан, endpoint-ы созданы, связи созданы
|
||||
# detected_service.status = accepted
|
||||
# Карточка сервиса показывает связи
|
||||
|
||||
---
|
||||
|
||||
## Итерация 8. Nginx/Cron/Backup сканеры
|
||||
|
||||
Цель: находить nginx vhost-ы, cron-задачи, backup-подсказки.
|
||||
|
||||
Что делаем:
|
||||
- NginxScanner: парсинг server_name, listen, proxy_pass, root
|
||||
- CronScanner: crontab -l, /etc/crontab, /etc/cron.d/*
|
||||
- BackupHintScanner: поиск rsync, borg, restic, tar, mysqldump...
|
||||
- detected_services kind=nginx_vhost, cron_job, backup_hint
|
||||
- Миграция domains
|
||||
- Создание domain suggestions из nginx server_name
|
||||
- Host scan обогащается nginx/cron/backup сканами
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateDomains.php
|
||||
app/Services/HostScan/NginxScanner.php
|
||||
app/Services/HostScan/CronScanner.php
|
||||
app/Services/HostScan/BackupHintScanner.php
|
||||
app/Repositories/DomainRepository.php
|
||||
templates/host_scans/show.php (добавить nginx/cron/backup)
|
||||
templates/devices/show.php (найденные сервисы)
|
||||
|
||||
Проверка:
|
||||
# Deep scan на хосте с nginx + cron + backup scripts
|
||||
# Увидеть detected_services: nginx_vhost, cron_job, backup_hint
|
||||
# Формулировка backup_hint: "Похоже на backup job"
|
||||
# Domains созданы из nginx server_name
|
||||
# Приватные ключи сертификатов НЕ прочитаны
|
||||
|
||||
---
|
||||
|
||||
## Итерация 9. Риски + Dashboard + История
|
||||
|
||||
Цель: предупреждения на dashboard, история сканов, diff.
|
||||
|
||||
Что делаем:
|
||||
- RiskAnalyzer: сервис без backup, публичный endpoint, SSH открыт,
|
||||
нет свежего scan, TLS истекает
|
||||
- Dashboard: реальные предупреждения
|
||||
- История scan jobs
|
||||
- DiffAnalyzer: новые/пропавшие хосты, порты, контейнеры между сканами
|
||||
- DiffAnalyzer: новые/пропавшие хосты, порты, контейнеры между сканами
|
||||
- Documents CRUD (заметки/runbooks)
|
||||
|
||||
Файлы (новые/изменённые):
|
||||
migrations/...CreateDocuments.php
|
||||
app/Controllers/DocumentController.php
|
||||
app/Controllers/ScanHistoryController.php
|
||||
app/Controllers/DiffController.php
|
||||
app/Services/Analysis/RiskAnalyzer.php
|
||||
app/Services/Analysis/DiffAnalyzer.php
|
||||
app/Repositories/DocumentRepository.php
|
||||
templates/dashboard/index.php (real warnings)
|
||||
templates/scans/history.php
|
||||
templates/scans/diff.php
|
||||
templates/documents/index.php
|
||||
templates/documents/create.php
|
||||
|
||||
Проверка:
|
||||
# Dashboard показывает предупреждения ( service без backup и т.д. )
|
||||
# История scan jobs с статусами
|
||||
# Diff между двумя сканами: новые/пропавшие хосты
|
||||
# Документы: создать runbook, привязать к устройству
|
||||
|
||||
---
|
||||
|
||||
## Итерация 10. Полировка + документация
|
||||
|
||||
Цель: финальная доводка проекта.
|
||||
|
||||
Что делаем:
|
||||
- Полный audit log для всех действий
|
||||
- Документация в README полностью
|
||||
- Скриншоты в README (опционально)
|
||||
- Проверка всех security-правил
|
||||
- scripts/check.sh обновлён под все итерации
|
||||
- Финальный smoke test: весь цикл scan → device → service
|
||||
|
||||
---
|
||||
|
||||
## Сводная таблица
|
||||
|
||||
Итерация | Что делаем | Зависимости
|
||||
1 | Каркас: Slim, Docker, auth, layout | —
|
||||
2 | Network scan | 1
|
||||
3 | Devices CRUD | 1
|
||||
4 | SSH credentials | 1, 3
|
||||
5 | Deep scan Linux | 1, 3, 4
|
||||
6 | Docker scan + detected_services | 1, 5
|
||||
7 | Services + relations | 1, 6
|
||||
8 | Nginx/Cron/Backup scanners | 1, 5, 7
|
||||
9 | Risks + Dashboard + History + Docs | 1-8
|
||||
10 | Полировка + README | 1-9
|
||||
|
||||
---
|
||||
|
||||
## Правила для каждой итерации
|
||||
|
||||
1. Не переходить к следующей, пока check.sh не прошёл.
|
||||
2. Показывать реальный вывод smoke test, а не "готово".
|
||||
3. Маленькие атомарные коммиты.
|
||||
4. Контроллеры — только роутинг, бизнес-логика в Services.
|
||||
5. SQL только в Repository-классах.
|
||||
6. Shell-команды только через CommandWhitelist (массивы аргументов).
|
||||
7. Никакого произвольного shell executor из UI.
|
||||
8. Никакого "чистый JS потом допишем" — весь JS заявлен заранее.
|
||||
|
||||
---
|
||||
|
||||
## Порядок выдачи задания owl-alpha
|
||||
|
||||
Рекомендуется выдавать по одной итерации:
|
||||
|
||||
Шаг 1: "Прочитай ТЗ и PLAN.md. Сделай Итерацию 1."
|
||||
Шаг 2: "Прочитай текущий код. Сделай Итерацию 2 по PLAN.md."
|
||||
...
|
||||
|
||||
После каждой итерации — ревью, smoke test, фикс замечаний,
|
||||
и только потом следующая итерация.
|
||||
|
||||
Никогда не давай больше одной итерации за раз.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# Домовой
|
||||
|
||||
Self-hosted система инвентаризации домашней и малой серверной инфраструктуры.
|
||||
|
||||
## Что делает
|
||||
|
||||
- Сканирует заданные локальные сетевые диапазоны
|
||||
- Находит устройства и предлагает создать карточки
|
||||
- После добавления SSH-доступа — read-only deep scan хоста
|
||||
- Находит сервисы, Docker-контейнеры, nginx vhost-ы, cron-задачи, backup-подсказки
|
||||
- Создаёт карточки сервисов и связи между устройствами и сервисами
|
||||
|
||||
## Стек
|
||||
|
||||
- PHP 8.3, Slim Framework 4, PDO
|
||||
- MariaDB
|
||||
- Bootstrap 5.3, htmx
|
||||
- Docker Compose
|
||||
- Phinx (миграции)
|
||||
|
||||
## Предупреждение
|
||||
|
||||
Сканируйте только свои сети. Приложение read-only и ничего не меняет на удалённых хостах.
|
||||
|
||||
## Установка
|
||||
|
||||
```bash
|
||||
git clone <repo>
|
||||
cd domovoy
|
||||
cp .env.example .env
|
||||
# Отредактируйте .env: задайте APP_SECRET, ENCRYPTION_KEY, DB_PASSWORD
|
||||
docker compose up -d --build
|
||||
docker compose exec app php vendor/bin/phinx migrate
|
||||
docker compose exec app php bin/setup first-admin
|
||||
# Откройте http://localhost:8080
|
||||
```
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
# http://localhost:8080
|
||||
```
|
||||
|
||||
## Миграции
|
||||
|
||||
```bash
|
||||
docker compose exec app php vendor/bin/phinx migrate
|
||||
docker compose exec app php vendor/bin/phinx rollback
|
||||
docker compose exec app php vendor/bin/phinx create MigrationName
|
||||
```
|
||||
|
||||
## Smoke test
|
||||
|
||||
```bash
|
||||
./scripts/check.sh
|
||||
```
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
domovoy/
|
||||
app/ # Controllers, Services, Repositories, Middleware
|
||||
db/migrations/ # Phinx миграции
|
||||
public/ # Точка входа, assets
|
||||
templates/ # PHP шаблоны
|
||||
bin/ # CLI команды
|
||||
docker/ # Dockerfile, nginx config
|
||||
storage/ # логи, scan results
|
||||
```
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Controllers;
|
||||
|
||||
use Domovoy\Services\AuthService;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class AuthController
|
||||
{
|
||||
private AuthService $authService;
|
||||
|
||||
public function __construct(AuthService $authService)
|
||||
{
|
||||
$this->authService = $authService;
|
||||
}
|
||||
|
||||
public function loginForm(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
if (isset($_SESSION['user_id'])) {
|
||||
return $response
|
||||
->withHeader('Location', '/dashboard')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
require dirname(__DIR__, 2) . '/templates/auth/login.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$username = $data['username'] ?? '';
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
$user = $this->authService->authenticate($username, $password);
|
||||
|
||||
if ($user === null) {
|
||||
ob_start();
|
||||
$error = 'Invalid username or password';
|
||||
require dirname(__DIR__, 2) . '/templates/auth/login.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response->withStatus(401);
|
||||
}
|
||||
|
||||
$_SESSION['user_id'] = $user->id;
|
||||
$_SESSION['username'] = $user->username;
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/dashboard')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
public function logout(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
session_destroy();
|
||||
return $response
|
||||
->withHeader('Location', '/login')
|
||||
->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DashboardController
|
||||
{
|
||||
public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
ob_start();
|
||||
$username = $_SESSION['username'] ?? 'User';
|
||||
require dirname(__DIR__, 2) . '/templates/dashboard/index.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Controllers;
|
||||
|
||||
use Domovoy\Services\AuthService;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class SetupController
|
||||
{
|
||||
private AuthService $authService;
|
||||
|
||||
public function __construct(AuthService $authService)
|
||||
{
|
||||
$this->authService = $authService;
|
||||
}
|
||||
|
||||
public function form(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
if ($this->authService->getUserCount() > 0) {
|
||||
return $response
|
||||
->withHeader('Location', '/login')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$error = null;
|
||||
require dirname(__DIR__, 2) . '/templates/auth/setup.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
if ($this->authService->getUserCount() > 0) {
|
||||
return $response
|
||||
->withHeader('Location', '/login')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
$data = $request->getParsedBody();
|
||||
$username = trim($data['username'] ?? '');
|
||||
$password = $data['password'] ?? '';
|
||||
|
||||
if (strlen($username) < 2 || strlen($password) < 8) {
|
||||
ob_start();
|
||||
$error = 'Username must be at least 2 chars, password at least 8';
|
||||
require dirname(__DIR__, 2) . '/templates/auth/setup.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $this->authService->createUser($username, $password);
|
||||
} catch (\RuntimeException $e) {
|
||||
ob_start();
|
||||
$error = $e->getMessage();
|
||||
require dirname(__DIR__, 2) . '/templates/auth/setup.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
$_SESSION['user_id'] = $user->id;
|
||||
$_SESSION['username'] = $user->username;
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/dashboard')
|
||||
->withStatus(302);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Middleware;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Slim\Routing\RouteContext;
|
||||
|
||||
class AuthMiddleware
|
||||
{
|
||||
public function __invoke(
|
||||
ServerRequestInterface $request,
|
||||
\Psr\Http\Server\RequestHandlerInterface $handler
|
||||
): ResponseInterface {
|
||||
$path = $request->getUri()->getPath();
|
||||
|
||||
$publicPaths = ['/login', '/setup'];
|
||||
$isPublic = in_array($path, $publicPaths, true);
|
||||
|
||||
if (!isset($_SESSION['user_id']) && !$isPublic) {
|
||||
$response = new \Slim\Psr7\Response();
|
||||
return $response
|
||||
->withHeader('Location', '/login')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
// If logged in and trying to access login/setup, redirect to dashboard
|
||||
if (isset($_SESSION['user_id']) && $isPublic) {
|
||||
$response = new \Slim\Psr7\Response();
|
||||
return $response
|
||||
->withHeader('Location', '/dashboard')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
return $handler->handle($request);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Models;
|
||||
|
||||
class User
|
||||
{
|
||||
public ?string $id = null;
|
||||
public string $username = '';
|
||||
public string $passwordHash = '';
|
||||
public ?\DateTimeImmutable $createdAt = null;
|
||||
public ?\DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public static function fromArray(array $data): self
|
||||
{
|
||||
$user = new self();
|
||||
$user->id = $data['id'];
|
||||
$user->username = $data['username'];
|
||||
$user->passwordHash = $data['password_hash'];
|
||||
$user->createdAt = new \DateTimeImmutable($data['created_at']);
|
||||
$user->updatedAt = new \DateTimeImmutable($data['updated_at']);
|
||||
return $user;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Repositories;
|
||||
|
||||
use PDO;
|
||||
use Domovoy\Models\User;
|
||||
|
||||
class UserRepository
|
||||
{
|
||||
private PDO $pdo;
|
||||
|
||||
public function __construct(PDO $pdo)
|
||||
{
|
||||
$this->pdo = $pdo;
|
||||
}
|
||||
|
||||
public function findById(string $id): ?User
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
|
||||
$stmt->execute(['id' => $id]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ? User::fromArray($row) : null;
|
||||
}
|
||||
|
||||
public function findByUsername(string $username): ?User
|
||||
{
|
||||
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE username = :username');
|
||||
$stmt->execute(['username' => $username]);
|
||||
$row = $stmt->fetch();
|
||||
return $row ? User::fromArray($row) : null;
|
||||
}
|
||||
|
||||
public function findAll(): array
|
||||
{
|
||||
$stmt = $this->pdo->query('SELECT * FROM users ORDER BY created_at DESC');
|
||||
$users = [];
|
||||
while ($row = $stmt->fetch()) {
|
||||
$users[] = User::fromArray($row);
|
||||
}
|
||||
return $users;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
$stmt = $this->pdo->query('SELECT COUNT(*) FROM users');
|
||||
return (int) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
public function save(User $user): void
|
||||
{
|
||||
if ($user->id === null) {
|
||||
$user->id = \Ramsey\Uuid\Uuid::uuid4()->toString();
|
||||
}
|
||||
|
||||
$now = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
|
||||
$user->updatedAt = new \DateTimeImmutable($now);
|
||||
if ($user->createdAt === null) {
|
||||
$user->createdAt = new \DateTimeImmutable($now);
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare(
|
||||
'INSERT INTO users (id, username, password_hash, created_at, updated_at)
|
||||
VALUES (:id, :username, :password_hash, :created_at, :updated_at)'
|
||||
);
|
||||
$stmt->execute([
|
||||
'id' => $user->id,
|
||||
'username' => $user->username,
|
||||
'password_hash' => $user->passwordHash,
|
||||
'created_at' => $user->createdAt->format('Y-m-d H:i:s'),
|
||||
'updated_at' => $user->updatedAt->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Services;
|
||||
|
||||
use Domovoy\Models\User;
|
||||
use Domovoy\Repositories\UserRepository;
|
||||
|
||||
class AuthService
|
||||
{
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
public function authenticate(string $username, string $password): ?User
|
||||
{
|
||||
$user = $this->userRepository->findByUsername($username);
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!password_verify($password, $user->passwordHash)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function createUser(string $username, string $password): User
|
||||
{
|
||||
if ($this->userRepository->findByUsername($username) !== null) {
|
||||
throw new \RuntimeException("User '{$username}' already exists");
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->username = $username;
|
||||
$user->passwordHash = password_hash($password, PASSWORD_ARGON2ID);
|
||||
$user->createdAt = new \DateTimeImmutable();
|
||||
$user->updatedAt = new \DateTimeImmutable();
|
||||
|
||||
$this->userRepository->save($user);
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function getUserCount(): int
|
||||
{
|
||||
return $this->userRepository->count();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use Domovoy\Services\AuthService;
|
||||
use Domovoy\Repositories\UserRepository;
|
||||
|
||||
$dotenv = \Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
$pdo = new PDO(
|
||||
sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
||||
getenv('DB_HOST') ?: 'db',
|
||||
getenv('DB_PORT') ?: 3306,
|
||||
getenv('DB_DATABASE') ?: 'domovoy'
|
||||
),
|
||||
getenv('DB_USERNAME') ?: 'domovoy',
|
||||
getenv('DB_PASSWORD') ?: 'domovoy',
|
||||
[
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
]
|
||||
);
|
||||
|
||||
$authService = new AuthService(new UserRepository($pdo));
|
||||
|
||||
$command = $argv[1] ?? 'help';
|
||||
|
||||
switch ($command) {
|
||||
case 'setup:user':
|
||||
if ($argv[2] ?? false) {
|
||||
// CLI mode: bin/console setup:user username password
|
||||
$username = $argv[2];
|
||||
$password = $argv[3] ?? 'password123';
|
||||
} else {
|
||||
// Interactive mode
|
||||
echo "Username: ";
|
||||
$username = trim(fgets(STDIN));
|
||||
echo "Password: ";
|
||||
$password = trim(fgets(STDIN));
|
||||
}
|
||||
|
||||
$user = $authService->createUser($username, $password);
|
||||
echo "User '{$user->username}' created successfully.\n";
|
||||
break;
|
||||
|
||||
case 'help':
|
||||
default:
|
||||
echo "Domovoy CLI\n\n";
|
||||
echo "Commands:\n";
|
||||
echo " setup:user [username] [password] Create first admin user\n";
|
||||
echo " help Show this help\n";
|
||||
break;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"name": "domovoy/domovoy",
|
||||
"description": "Self-hosted inventory system for home/small infrastructure",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=8.3",
|
||||
"slim/slim": "^4.14",
|
||||
"slim/psr7": "^1.7",
|
||||
"php-di/php-di": "^7.0",
|
||||
"monolog/monolog": "^3.0",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"symfony/process": "^7.0",
|
||||
"phpseclib/phpseclib": "~3.0",
|
||||
"defuse/php-encryption": "^2.4",
|
||||
"robmorgan/phinx": "^0.15"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^11.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Domovoy\\": "app/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Domovoy\\Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"migrate": "php vendor/bin/phinx migrate",
|
||||
"migrate:rollback": "php vendor/bin/phinx rollback",
|
||||
"migrate:create": "php vendor/bin/phinx create"
|
||||
},
|
||||
"config": {
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "8.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class CreateUsers extends AbstractMigration
|
||||
{
|
||||
public function change(): void
|
||||
{
|
||||
$table = $this->table('users', ['id' => false, 'primary_key' => ['id']]);
|
||||
$table
|
||||
->addColumn('id', 'uuid', ['null' => false])
|
||||
->addColumn('username', 'string', ['limit' => 255, 'null' => false])
|
||||
->addColumn('password_hash', 'string', ['limit' => 255, 'null' => false])
|
||||
->addColumn('created_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addColumn('updated_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP'])
|
||||
->addIndex(['username'], ['unique' => true])
|
||||
->create();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/app/Dockerfile
|
||||
container_name: domovoy-app
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
- ./storage:/var/www/html/storage
|
||||
expose:
|
||||
- "9000"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
- APP_ENV=${APP_ENV:-development}
|
||||
- APP_SECRET=${APP_SECRET}
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||
- DB_HOST=${DB_HOST:-db}
|
||||
- DB_PORT=${DB_PORT:-3306}
|
||||
- DB_DATABASE=${DB_DATABASE:-domovoy}
|
||||
- DB_USERNAME=${DB_USERNAME:-domovoy}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-domovoy}
|
||||
|
||||
web:
|
||||
image: nginx:alpine
|
||||
container_name: domovoy-web
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
- ./docker/web/default.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
db:
|
||||
image: mariadb:11
|
||||
container_name: domovoy-db
|
||||
ports:
|
||||
- "3307:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-rootpass}
|
||||
- MYSQL_DATABASE=${DB_DATABASE:-domovoy}
|
||||
- MYSQL_USER=${DB_USERNAME:-domovoy}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD:-domovoy}
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
FROM php:8.3-fpm
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
mariadb-client \
|
||||
iproute2 \
|
||||
iputils-ping \
|
||||
net-tools \
|
||||
dnsutils \
|
||||
openssh-client \
|
||||
openssl \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN docker-php-ext-install pdo pdo_mysql
|
||||
|
||||
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
RUN install-php-extensions zip mbstring xml curl
|
||||
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Copy vendor from host (pre-installed with correct PHP version)
|
||||
COPY vendor/ vendor/
|
||||
COPY composer.json composer.lock ./
|
||||
|
||||
RUN mkdir -p storage/logs storage/scans storage/reports \
|
||||
&& chown -R www-data:www-data storage
|
||||
|
||||
EXPOSE 9000
|
||||
CMD ["php-fpm"]
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /var/www/html/public;
|
||||
index index.php;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass app:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
'paths' => [
|
||||
'migrations' => __DIR__ . '/db/migrations',
|
||||
'seeds' => __DIR__ . '/db/seeds',
|
||||
],
|
||||
'environments' => [
|
||||
'default_migration_table' => 'phinxlog',
|
||||
'default_environment' => 'development',
|
||||
'production' => [
|
||||
'adapter' => 'mysql',
|
||||
'host' => getenv('DB_HOST') ?: 'db',
|
||||
'port' => getenv('DB_PORT') ?: 3306,
|
||||
'name' => getenv('DB_DATABASE') ?: 'domovoy',
|
||||
'user' => getenv('DB_USERNAME') ?: 'domovoy',
|
||||
'pass' => getenv('DB_PASSWORD') ?: 'domovoy',
|
||||
'charset' => 'utf8mb4',
|
||||
],
|
||||
'development' => [
|
||||
'adapter' => 'mysql',
|
||||
'host' => getenv('DB_HOST') ?: 'db',
|
||||
'port' => getenv('DB_PORT') ?: 3306,
|
||||
'name' => getenv('DB_DATABASE') ?: 'domovoy',
|
||||
'user' => getenv('DB_USERNAME') ?: 'domovoy',
|
||||
'pass' => getenv('DB_PASSWORD') ?: 'domovoy',
|
||||
'charset' => 'utf8mb4',
|
||||
],
|
||||
],
|
||||
'version_order' => 'creation',
|
||||
];
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/* Domovoy custom styles */
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .list-group-item:hover {
|
||||
background-color: #343a40 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
#sidebar-wrapper .list-group-item.active {
|
||||
background-color: #0d6efd !important;
|
||||
border-color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
session_start();
|
||||
|
||||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||||
|
||||
use DI\ContainerBuilder;
|
||||
use Slim\Factory\AppFactory;
|
||||
|
||||
$dotenv = \Dotenv\Dotenv::createImmutable(dirname(__DIR__));
|
||||
$dotenv->safeLoad();
|
||||
|
||||
// Build container
|
||||
$containerBuilder = new ContainerBuilder();
|
||||
$containerBuilder->addDefinitions([
|
||||
'settings' => [
|
||||
'db' => [
|
||||
'host' => getenv('DB_HOST') ?: 'db',
|
||||
'port' => (int)(getenv('DB_PORT') ?: 3306),
|
||||
'database' => getenv('DB_DATABASE') ?: 'domovoy',
|
||||
'username' => getenv('DB_USERNAME') ?: 'domovoy',
|
||||
'password' => getenv('DB_PASSWORD') ?: 'domovoy',
|
||||
],
|
||||
'app' => [
|
||||
'env' => getenv('APP_ENV') ?: 'development',
|
||||
'secret' => getenv('APP_SECRET') ?: 'change-me',
|
||||
'encryption_key' => getenv('ENCRYPTION_KEY') ?: '',
|
||||
],
|
||||
'ssh' => [
|
||||
'connect_timeout' => (int)(getenv('SSH_CONNECT_TIMEOUT_SECONDS') ?: 5),
|
||||
'auth_timeout' => (int)(getenv('SSH_AUTH_TIMEOUT_SECONDS') ?: 10),
|
||||
'command_timeout' => (int)(getenv('SSH_COMMAND_TIMEOUT_SECONDS') ?: 8),
|
||||
'total_scan_timeout' => (int)(getenv('SSH_TOTAL_SCAN_TIMEOUT_SECONDS') ?: 60),
|
||||
'retry_count' => (int)(getenv('SSH_RETRY_COUNT') ?: 0),
|
||||
],
|
||||
],
|
||||
'logger' => function () {
|
||||
$logger = new \Monolog\Logger('domovoy');
|
||||
$logger->pushHandler(new \Monolog\Handler\StreamHandler(
|
||||
dirname(__DIR__) . '/storage/logs/app.log',
|
||||
\Monolog\Level::Debug
|
||||
));
|
||||
return $logger;
|
||||
},
|
||||
PDO::class => function ($c) {
|
||||
$db = $c->get('settings')['db'];
|
||||
$dsn = sprintf(
|
||||
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
||||
$db['host'],
|
||||
$db['port'],
|
||||
$db['database']
|
||||
);
|
||||
return new PDO($dsn, $db['username'], $db['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
},
|
||||
\Domovoy\Repositories\UserRepository::class => function ($c) {
|
||||
return new \Domovoy\Repositories\UserRepository($c->get(PDO::class));
|
||||
},
|
||||
\Domovoy\Services\AuthService::class => function ($c) {
|
||||
return new \Domovoy\Services\AuthService($c->get(\Domovoy\Repositories\UserRepository::class));
|
||||
},
|
||||
\Domovoy\Controllers\AuthController::class => function ($c) {
|
||||
return new \Domovoy\Controllers\AuthController($c->get(\Domovoy\Services\AuthService::class));
|
||||
},
|
||||
\Domovoy\Controllers\SetupController::class => function ($c) {
|
||||
return new \Domovoy\Controllers\SetupController($c->get(\Domovoy\Services\AuthService::class));
|
||||
},
|
||||
\Domovoy\Controllers\DashboardController::class => function () {
|
||||
return new \Domovoy\Controllers\DashboardController();
|
||||
},
|
||||
]);
|
||||
$container = $containerBuilder->build();
|
||||
|
||||
// Build Slim app
|
||||
AppFactory::setContainer($container);
|
||||
$app = AppFactory::create();
|
||||
|
||||
$app->addRoutingMiddleware();
|
||||
$app->addBodyParsingMiddleware();
|
||||
$app->add(new \Domovoy\Middleware\AuthMiddleware());
|
||||
$app->addErrorMiddleware(true, true, true);
|
||||
|
||||
// Routes
|
||||
$app->get('/login', [\Domovoy\Controllers\AuthController::class, 'loginForm'])->setName('login');
|
||||
$app->post('/login', [\Domovoy\Controllers\AuthController::class, 'login'])->setName('login.post');
|
||||
$app->get('/setup', [\Domovoy\Controllers\SetupController::class, 'form'])->setName('setup');
|
||||
$app->post('/setup', [\Domovoy\Controllers\SetupController::class, 'create'])->setName('setup.post');
|
||||
|
||||
$app->group('', function (\Slim\Routing\RouteCollectorProxy $group) {
|
||||
$group->get('/dashboard', [\Domovoy\Controllers\DashboardController::class, 'index'])->setName('dashboard');
|
||||
$group->post('/logout', [\Domovoy\Controllers\AuthController::class, 'logout'])->setName('logout');
|
||||
});
|
||||
|
||||
$app->run();
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== composer validate ==="
|
||||
composer validate --no-check-publish
|
||||
|
||||
echo "=== composer install ==="
|
||||
composer install --no-interaction --prefer-dist
|
||||
|
||||
echo "=== PHP syntax check ==="
|
||||
find app public bin -name "*.php" -print0 | xargs -0 -n1 php -l
|
||||
|
||||
echo "=== docker compose config ==="
|
||||
docker compose config > /dev/null
|
||||
|
||||
echo "=== docker compose up ==="
|
||||
docker compose up -d --build
|
||||
|
||||
echo "=== wait for db ==="
|
||||
sleep 5
|
||||
|
||||
echo "=== phinx migrate ==="
|
||||
docker compose exec app php vendor/bin/phinx migrate
|
||||
|
||||
echo "=== HTTP health check ==="
|
||||
curl -sI http://localhost:8080/login | head -1
|
||||
|
||||
echo "=== ALL CHECKS PASSED ==="
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php ob_start(); ?>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-center mb-4">Вход в Домовой</h4>
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="/login">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Имя пользователя</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Пароль</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Войти</button>
|
||||
</form>
|
||||
<div class="text-center mt-3">
|
||||
<small class="text-muted">
|
||||
Нет аккаунта? <a href="/setup">Создать первого пользователя</a>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $content = ob_get_clean(); ?>
|
||||
<?php require dirname(__DIR__) . '/layout.php'; ?>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php ob_start(); ?>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<div class="card mt-5">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title text-center mb-4">Создание первого пользователя</h4>
|
||||
<p class="text-muted text-center">Это будет администратор системы "Домовой"</p>
|
||||
<?php if (isset($error)): ?>
|
||||
<div class="alert alert-danger"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<form method="POST" action="/setup">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Имя пользователя</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required autofocus
|
||||
minlength="2" maxlength="255">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Пароль</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required
|
||||
minlength="8">
|
||||
<div class="form-text">Минимум 8 символов</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Создать аккаунт</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php $content = ob_get_clean(); ?>
|
||||
<?php require dirname(__DIR__) . '/layout.php'; ?>
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<?php ob_start(); ?>
|
||||
<h2>Dashboard</h2>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-primary">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Устройства</h5>
|
||||
<p class="card-text display-6">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-success">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Сервисы</h5>
|
||||
<p class="card-text display-6">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-info">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Новые находки</h5>
|
||||
<p class="card-text display-6">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-warning">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Требуют внимания</h5>
|
||||
<p class="card-text display-6">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
Последние найденные хосты
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Нет данных. Запустите сканирование сети.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
Последние события
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Нет событий.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
Последний скан сети
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Сканирование ещё не запускалось.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php $content = ob_get_clean(); ?>
|
||||
<?php require dirname(__DIR__) . '/layout.php'; ?>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Домовой</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="/assets/css/app.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div class="d-flex" id="wrapper">
|
||||
<!-- Sidebar -->
|
||||
<nav class="bg-dark text-white border-end" id="sidebar-wrapper" style="width: 250px; min-height: 100vh;">
|
||||
<div class="sidebar-heading p-3 border-bottom border-secondary">
|
||||
<a href="/dashboard" class="text-white text-decoration-none">
|
||||
<h4 class="mb-0">Домовой</h4>
|
||||
</a>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="/dashboard" class="list-group-item list-group-item-action bg-dark text-white border-0">
|
||||
Dashboard
|
||||
</a>
|
||||
<a href="/discovery" class="list-group-item list-group-item-action bg-dark text-white border-0">
|
||||
Сканирование сети
|
||||
</a>
|
||||
<a href="/devices" class="list-group-item list-group-item-action bg-dark text-white border-0">
|
||||
Устройства
|
||||
</a>
|
||||
<a href="/services" class="list-group-item list-group-item-action bg-dark text-white border-0">
|
||||
Сервисы
|
||||
</a>
|
||||
<a href="/documents" class="list-group-item list-group-item-action bg-dark text-white border-0">
|
||||
Документы
|
||||
</a>
|
||||
<a href="/settings" class="list-group-item list-group-item-action bg-dark text-white border-0">
|
||||
Настройки
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div id="page-content-wrapper" class="w-100">
|
||||
<!-- Top Navbar -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark border-bottom border-secondary">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand text-light">Домовой</span>
|
||||
<div class="ms-auto d-flex align-items-center">
|
||||
<span class="text-light me-3"><?= htmlspecialchars($username ?? 'User') ?></span>
|
||||
<form method="POST" action="/logout" class="d-inline">
|
||||
<button type="submit" class="btn btn-outline-light btn-sm">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="container-fluid p-4">
|
||||
<?php if (isset($content)): ?>
|
||||
<?= $content ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue