Files
ridebus-admin/README.md

418 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# RideBus DE API
Система управления информацией об общественном транспорте города Полоцка с REST API, веб-панелью администратора и поддержкой offline-синхронизации.
## 🚀 Возможности
### MVP (Версия 1.0 - статические данные)
- ✅ REST API для маршрутов и остановок
- ✅ Геопространственные запросы (PostGIS)
- ✅ Расчет ETA на основе расписания
- ✅ Offline-синхронизация для мобильных приложений
- ✅ Веб-панель администратора
- ✅ Кеширование через Redis
- ✅ Аутентификация по API ключам
- ✅ Rate limiting
- ✅ Серверные push-уведомления об алертах через Firebase Cloud Messaging
- ✅ Импорт отмен из Telegram через отдельный аккаунт, проверка в админке и срок действия до полуночи ([настройка](docs/telegram-cancellation-import.md))
### Технологический стек
**Backend:**
- Node.js + Express
- PostgreSQL + PostGIS
- Redis
- Docker
**Frontend (Admin Panel):**
- React + Vite
- Tailwind CSS
- React Leaflet (карты)
- Axios
## 📋 Требования
- Docker и Docker Compose
- Node.js 22+ (для разработки без Docker и Firebase Admin SDK)
- Статический IP или динамический DNS
- ~2GB свободного места на диске
## 🛠 Установка и запуск
### 1. Клонирование и настройка
```bash
# Создайте .env файл из примера
cp .env.example .env
# Настройте переменные окружения
nano .env
```
Важные переменные в `.env`:
```env
# Обязательно измените в продакшене!
DB_PASSWORD=your_secure_password_here
API_KEY_SALT=generate_random_salt_with_openssl
ADMIN_PASSWORD=your_secure_admin_password_here
# Для разработки/деплоя через встроенный proxy оставьте как есть
CORS_ORIGIN=http://localhost:3001
API_URL=/api/v1
# Push отключён по умолчанию
FCM_ENABLED=false
# Для пассажирского режима backend проверяет Firebase ID token.
# Укажите проект из google-services.json мобильного приложения и путь к service account JSON.
FIREBASE_PROJECT_ID=your-firebase-project-id
GOOGLE_APPLICATION_CREDENTIALS=/absolute/private/path/firebase-service-account.json
```
Для входа по passkey на публичном адресе укажите в `.env` точный домен панели
и используйте HTTPS: `WEBAUTHN_RP_ID=admin.example.by` и
`WEBAUTHN_ORIGIN=https://admin.example.by`. В `WEBAUTHN_ORIGIN` не применяются
wildcard-маски; при нескольких разрешённых адресах перечислите их через запятую.
Генерация безопасного salt:
```bash
openssl rand -base64 32
```
### 2. Запуск с Docker (рекомендуется)
```bash
# Сборка и запуск всех сервисов
docker-compose up -d
# Проверка статуса
docker-compose ps
# Просмотр логов
docker-compose logs -f
```
Сервисы будут доступны:
- **API**: http://localhost:3000
- **Admin Panel**: http://localhost:3001
- **PostgreSQL**: localhost:5432
- **Redis**: localhost:6379
Мастер-аккаунт создается автоматически при старте backend:
- login: `admin`
- password: значение `ADMIN_PASSWORD` из `.env` (если не задано, fallback `admin123`)
### 3. Проверка работоспособности
```bash
# Проверка API
curl http://localhost:3000/health
# Проверка базы данных
docker exec -it polotsk-transit-db psql -U transit_admin -d polotsk_transit -c "SELECT COUNT(*) FROM routes;"
```
### 4. Разработка без Docker
**Backend:**
```bash
cd backend
npm install
cp ../.env.example .env
# Убедитесь, что PostgreSQL и Redis запущены локально
# Настройте DATABASE_URL и REDIS_URL в .env
npm run dev
```
**Frontend:**
```bash
cd frontend
npm install
cp .env.example .env
npm run dev
```
## 📱 Использование API
### Аутентификация
Все запросы требуют API ключ в заголовке:
```bash
curl -H "X-API-Key: dev_key_12345678" http://localhost:3000/api/v1/routes
```
### Основные эндпоинты
**Маршруты:**
```bash
GET /api/v1/routes # Все маршруты
GET /api/v1/routes/:id # Маршрут с остановками
POST /api/v1/routes # Создать маршрут
PUT /api/v1/routes/:id # Обновить маршрут
DELETE /api/v1/routes/:id # Удалить маршрут
```
**Остановки:**
```bash
GET /api/v1/stops # Все остановки
GET /api/v1/stops/:id # Конкретная остановка
GET /api/v1/stops/nearby?lat=55.4869&lon=28.7856&radius=500 # Ближайшие
POST /api/v1/stops # Создать остановку
PUT /api/v1/stops/:id # Обновить остановку
DELETE /api/v1/stops/:id # Удалить остановку
```
**ETA (расчет времени прибытия):**
```bash
POST /api/v1/eta/calculate
{
"stopId": 1,
"routeId": 1
}
GET /api/v1/eta/stop/:stopId # ETA для всех маршрутов на остановке
```
**Синхронизация (для offline-режима):**
```bash
GET /api/v1/sync # Полная синхронизация
GET /api/v1/sync?lastSync=2025-11-19T10:00:00Z # Инкрементальная
GET /api/v1/sync/status # Статус системы
```
**Алерты:**
```bash
GET /api/v1/alerts/active # Активные алерты для клиентов
POST /api/v1/alerts # Создать алерт и, если он активен, отправить FCM
```
Настройка Firebase, контракт сообщения, идемпотентность и способы локальной
проверки описаны в [документации FCM-уведомлений](docs/fcm-alert-notifications.md).
### Примеры запросов
**Создание маршрута:**
```bash
curl -X POST http://localhost:3000/api/v1/routes \
-H "X-API-Key: dev_key_12345678" \
-H "Content-Type: application/json" \
-d '{
"route_number": "1",
"name": "Центр - Вокзал",
"type": "bus",
"color": "#0066CC",
"description": "Основной маршрут через центр города"
}'
```
**Создание остановки:**
```bash
curl -X POST http://localhost:3000/api/v1/stops \
-H "X-API-Key: dev_key_12345678" \
-H "Content-Type: application/json" \
-d '{
"name": "Площадь Свободы",
"latitude": 55.4869,
"longitude": 28.7856,
"address": "ул. Ленина, 1"
}'
```
**Поиск ближайших остановок:**
```bash
curl -H "X-API-Key: dev_key_12345678" \
"http://localhost:3000/api/v1/stops/nearby?lat=55.4869&lon=28.7856&radius=500"
```
## 🔐 Безопасность домашнего сервера
### Обязательные меры безопасности
1. **Cloudflare Tunnel** (рекомендуется):
```bash
# Установка cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
sudo mv cloudflared /usr/local/bin/
sudo chmod +x /usr/local/bin/cloudflared
# Аутентификация
cloudflared tunnel login
# Создание туннеля
cloudflared tunnel create polotsk-transit
# Запуск
cloudflared tunnel run polotsk-transit
```
2. **Настройка SSL в Nginx:**
```bash
# Получение сертификата Let's Encrypt
sudo certbot --nginx -d api.yourdomain.com -d admin.yourdomain.com
# Сертификаты автоматически обновятся
```
3. **Fail2ban:**
```bash
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
```
4. **Мониторинг:**
```bash
# Установка Prometheus + Grafana (опционально)
# Или простой мониторинг через Docker stats
docker stats
```
### Создание API ключей
API ключи можно создать через базу данных:
```sql
INSERT INTO api_keys (key, app_name, rate_limit)
VALUES ('your_secure_key_here', 'iOS App', 10000);
```
Или через скрипт:
```javascript
const crypto = require('crypto');
const apiKey = crypto.randomBytes(32).toString('hex');
console.log(apiKey);
```
## 📊 Админ-панель
Откройте http://localhost:3001 в браузере.
**Функции:**
- Управление маршрутами
- Создание и редактирование остановок на карте
- Просмотр статистики
- Управление расписаниями (в разработке)
## 🗄 Структура базы данных
```sql
routes # Маршруты
stops # Остановки (с PostGIS геометрией)
route_stops # Связь маршрутов и остановок
schedules # Расписания
vehicles # Транспортные средства
historical_delays # Статистика задержек
alerts # Уведомления
api_keys # API ключи
sync_log # Лог изменений для синхронизации
```
## 🔄 Обновления
```bash
# Остановка сервисов
docker-compose down
# Обновление кода
git pull
# Перезапуск
docker-compose up -d --build
```
## 🐛 Troubleshooting
**База данных не стартует:**
```bash
docker-compose logs postgres
docker-compose down -v # Удалит volumes, ОСТОРОЖНО!
docker-compose up -d
```
**API не отвечает:**
```bash
docker-compose logs backend
docker-compose restart backend
```
**Frontend не собирается:**
```bash
cd frontend
npm install
npm run build
```
**PostGIS ошибки:**
```bash
docker exec -it polotsk-transit-db psql -U transit_admin -d polotsk_transit
SELECT PostGIS_Version();
```
## 📝 Следующие шаги (Roadmap)
### Версия 2.0 - Краудсорсинг GPS
- [ ] Endpoint для получения GPS от пользователей
- [ ] Агрегация и фильтрация GPS данных
- [ ] WebSocket для реалтайм-обновлений
- [ ] Функция "Я еду в маршруте" в приложениях
### Версия 3.0 - Продвинутые функции
- [ ] IoT GPS-трекеры интеграция
- [ ] Машинное обучение для предсказания задержек
- [x] Push-уведомления о новых активных алертах
- [ ] Аналитика и отчеты
## 🤝 Контакты и поддержка
Создано для города Полоцка 🇧🇾
## 📄 Лицензия
MIT License - используйте свободно!
## Устройства RideBus Board
В админке появился раздел **Устройства** (только для администратора). Он работает с обновлённым проектом `ridebus-board`: одноразовый код из четырёх цифр на экране привязывает отдельный браузер телевизора. В форме выбираются площадка и маршрут, тема/палитры, профиль читаемости, число строк, интервалы, информационные блоки и рекламный предпросмотр. Город задаётся переключателем транспортной сети в админке; устройства и каталоги изолированы по выбранному городу.
Перед запуском примените **только новую** миграцию `backend/migrations/016_board_devices.sql` к существующей базе данных (обычным способом миграций при развёртывании). Не перезапускайте общий `npm run migrate` на уже созданной базе: существующий скрипт повторяет все старые SQL-файлы. Таблицы `board_pairings` и `board_devices` содержат одноразовые сессии и зарегистрированные устройства. Миграция 016 повторяемая.
В сервере Board включите `BOARD_REMOTE_SETTINGS=true`, укажите обновлённый API в `RIDEBUS_API_BASE_URL` и адрес этой админки в необязательном `BOARD_ADMIN_URL`. Серверный RideBus API-ключ остаётся на Board и не выдаётся телевизору. Используйте HTTPS с Secure cookie.
Новый узкий API монтируется до общей API-key авторизации:
- `POST /api/v1/board-devices/pairings` — создать сессию, TTL 5 минут, до 10 запросов за 10 минут с одного IP.
- `GET /api/v1/board-devices/self` и `POST /self/heartbeat` — только индивидуальный `X-Board-Token`.
- `GET /api/v1/board-devices/admin` и `/admin/routes?stopId=…` — администратор + контекст города.
- `POST /admin/claim` — код, название, настройки; до 5 попыток за 5 минут с одного IP.
- `PUT /admin/:id` — настройки с ожидаемой `revision`; конфликт даёт 409.
- `POST /admin/:id/revoke` — отозвать устройство в выбранном городе.
Короткий код не является токеном доступа. Секрет — 32 случайных байта, в базе только хеш; занятие кода и создание устройства атомарны в одной транзакции. Повторный запрос устройства после потерянного HTTP-ответа узнаёт уже созданную привязку. В списках и конфигурации секреты не возвращаются.
Табло опрашивает настройки раз в 15 секунд, отправляет сигнал работы раз в 30 и подтверждает применённую версию. «В сети» означает связь за последние 90 секунд. Офлайн-устройство продолжает работать со своим кешем, изменения остаются «Ожидают применения». Отзыв доступа действует после следующей связи с сервером. Команд перезагрузки ОС, удалённого shell или установки ПО нет.
Проверки: `npm test --prefix backend`, `npm run build --prefix frontend`. Отдельный PostgreSQL-тест устройств запускается с `BOARD_DEVICE_TEST_DATABASE_URL` (имя БД должно содержать `test`); он создаёт и удаляет изолированную схему, не использует рабочие таблицы. Совместный визуальный стенд находится в `ridebus-board/test/fixtures/device-flow-server.mjs` и использует явные тестовые данные.
### Ночной режим табло
В настройках устройства доступны `nightMode` (`auto` / `off`) и `nightWakeMinutes` (45 / 60). По умолчанию — автоматически, возврат за 60 минут. Обновлённый Board включает режим после завершения движения, если весь ночной перерыв до первого следующего рейса длиннее двух часов, только по полному действующему расписанию. Перезагрузка в середине перерыва сохраняет ночной экран до выбранного порога возврата 45/60 минут. Ночные продолжения рейсов, фильтр маршрута и отмены учитываются. В ночном экране остаются первый рейс, часы без секунд и сообщения о движении; цикл информации и рекламы приостанавливается. Дневная тема возвращается при выходе. Лимиты, привязка и подтверждение версии сохраняются.
Поля находятся в существующем JSONB настроек устройств; новая SQL-миграция не требуется. Старые сохранённые конфигурации нормализуются при чтении (`auto / 60`) и не изменяются в базе автоматически. Для редактирования этих полей разверните обновлённые API, frontend админки и Board вместе.
Блок информации «Дальше по маршруту» заменяет первый и последний рейс. Его переключатель сохраняет существующий ключ `infoServiceEnabled`: ранее отключённый блок остаётся отключённым, SQL-миграция и изменение JSON устройства не нужны. Схему следующих остановок и время пути строит обновлённый Board из полного кеша расписания; в админке изменено только название переключателя.
### Пассажирские блоки табло
В форме устройства добавлены переключатели «До важных мест», «Оплата поездки», «Напоминание о последнем рейсе» и «Вероятность осадков». До трёх важных мест задаются конкретной остановкой назначения и коротким названием. По умолчанию места не выбраны; незаполненный блок на табло скрыт. Отправление, путь и прибытие рассчитывает Board из полного действующего расписания с порядком остановок и конечной конкретного рейса. Отмены, направление и переход через полночь учитываются.
Тарифы по типу транспорта имеют поля `cityId`, `type` (`bus` / `minibus`), `price`, `payment`, `checkedAt`. Для Полоцка (город 1) пользователь подтвердил 11 октября 2026 года: автобус 1,05 BYN, маршрутка 2,10 BYN, оплата QR-кодом или наличными. Для других городов тарифы остаются незаполненными. Допустимы до 32 исключений по `routeId`; исключение имеет приоритет над тарифом типа. Цена и способы оплаты — подтверждённый оператором текст, дата проверки сохраняется и видна пассажиру. Будущий тариф не публикуется до указанной даты. Платёжный QR-код не генерируется.
Конфигурация: `infoPlacesEnabled`, `infoFareEnabled`, `lastTripNoticeEnabled`, `weatherHintEnabled` (по умолчанию `true`); `places: [{stopId, name}]`; `fareByType: [{cityId, type, price, payment, checkedAt}]`; `fareRules: [{routeId, price, payment, checkedAt}]`. API проверяет структуру, лимиты, дубликаты, действительность даты и принадлежность городу/площадке. Явные пустые массивы сохраняются. Старые конфигурации нормализуются без записи в БД; ранее отсутствовавший тариф типа подставляется только для города 1.
Новые поля хранятся в существующем JSONB; дополнительная SQL-миграция не нужна. Разверните обновлённые backend, frontend админки и RideBus Board вместе. Переключатели и содержимое передаются через существующие версии настроек и подтверждения устройством. Проверки пассажирских настроек входят в `test/boardDevices.test.js` и `test/boardDevicesApi.test.js`; визуальный стенд `ridebus-board/test/fixtures/passenger-server.mjs` использует отдельный локальный origin и тестовое расписание.