418 lines
22 KiB
Markdown
418 lines
22 KiB
Markdown
# 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 и тестовое расписание.
|