You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1555 lines
73 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Обзор проекта
`polotsk-transit-api` — это монорепозиторий с backend API на Node.js/Express и frontend-админкой на React/Vite. По коду проект сейчас реализует панель управления справочными и операционными данными общественного транспорта: маршруты, остановки, связки маршрут-остановка, расписания, ETA, праздничные дни, уведомления, пользователей и offline-синхронизацию.
Фактически найдено:
- Backend API: Express REST API, версия `v1`
- Frontend: SPA-админка на React, использующая тот же API
- Хранилище: PostgreSQL + PostGIS
- Кеш/вспомогательный слой: Redis
- Reverse proxy: Nginx
- Статические файлы: аватары пользователей через `/uploads/...`
Не найдено в реальной реализации:
- GraphQL schema/resolvers
- RPC endpoints
- WebSocket/realtime endpoints
- API для водителей
- API для диспетчеров
- API для GPS/телеметрии
- API для статусов рейсов/trip management
- API отчетности
Отдельно: в SQL-схеме есть сущность `vehicles`, а в ETA используется таблица `historical_delays`, но публичных CRUD-endpoints для этих сущностей в коде нет.
# Архитектура
## Высокоуровневая схема
- `frontend/` — React/Vite SPA для операторов и администраторов
- `backend/` — Express API с REST-маршрутами
- `backend/migrations/` — SQL-схема БД, функции и триггеры
- `nginx/` — reverse proxy и rate limiting на уровне веб-сервера
- `docker-compose.yml` — оркестрация Postgres, Redis, backend, frontend и Nginx
## Backend поток запроса
1. Nginx проксирует запросы на backend или frontend.
2. Express применяет `helmet`, `cors`, `compression`, body parsers и request logging.
3. Для `/api/v1/auth/*` действует отдельный rate limit.
4. Для `/api/v1/upload/*` действует JWT-аутентификация и отдельный rate limit.
5. Для `/api/v1/*` действует middleware `authenticateApiKey`, который принимает либо JWT Bearer token, либо `X-API-Key`.
6. Контроллеры работают с PostgreSQL напрямую через `pg`.
7. Отдельные read-маршруты и карточки сущностей кешируются в Redis.
8. Изменения основных таблиц пишутся в `sync_log` триггерами БД для offline sync.
## Версионирование
- Найдена только одна версия API: `/api/v1`
## Реализация API
- Только REST over HTTP
- JSON для большинства endpoints
- `multipart/form-data` для загрузки аватаров
# Основные модули
## Backend-модули
- `backend/src/index.js` — точка входа приложения, middleware, mount routes, global error handler
- `backend/src/routes/auth.js` + `backend/src/controllers/authController.js` — JWT login/refresh/logout/profile/password
- `backend/src/routes/users.js` + `backend/src/controllers/usersController.js` — управление пользователями
- `backend/src/routes/upload.js` + `backend/src/controllers/uploadController.js` — загрузка/удаление аватаров
- `backend/src/routes/api.js` — основной REST router `v1`
- `backend/src/controllers/routesController.js` — CRUD маршрутов
- `backend/src/controllers/stopsController.js` — CRUD остановок и геопоиск через PostGIS
- `backend/src/controllers/routeStopsController.js` — управление остановками внутри маршрута
- `backend/src/controllers/schedulesController.js` — расписания маршрутов
- `backend/src/controllers/holidaysController.js` — праздники и календарная логика
- `backend/src/controllers/etaController.js` — ETA на основе расписания и исторических задержек
- `backend/src/controllers/syncController.js` — full/incremental offline sync
- `backend/src/routes/alerts.js` — CRUD уведомлений
- `backend/src/middleware/auth.js` — JWT/API-key auth + admin guard
- `backend/src/middleware/rateLimiter.js` — rate limiting
- `backend/src/middleware/upload.js` — multer storage/filter/size limits
## Frontend-модули
- `frontend/src/services/api.js` — axios client, auth interceptors, API surface
- `frontend/src/contexts/AuthContext.jsx` — login, refresh, logout, profile bootstrap
- `frontend/src/components/ProtectedRoute.jsx` — frontend RBAC guard
- `frontend/src/pages/*.jsx` — операционные экраны для маршрутов, остановок, расписаний, праздников, уведомлений, пользователей и профиля
## Инфраструктурные модули
- `backend/src/config/database.js` — connection pool PostgreSQL
- `backend/src/config/redis.js` — Redis client
- `backend/src/config/logger.js` — Winston logging
- `backend/migrations/001_init.sql` — основная схема БД и sync triggers
- `backend/migrations/002_holidays.sql` — праздники Беларуси + SQL functions `is_holiday()` и `get_schedule_type()`
- `backend/migrations/003_users_auth.sql` — пользователи и refresh sessions
- `backend/migrations/004_users_avatars.sql` — аватары пользователей
- `backend/scripts/create-admin.js` — bootstrap admin account
# Аутентификация и авторизация
## Найденные механизмы auth
- `X-API-Key` для machine-to-machine и общих API-вызовов
- `Authorization: Bearer <JWT>` для операторов/админов админ-панели
- refresh token хранится в таблице `user_sessions`
## Реальное поведение middleware
- `/health` — без аутентификации
- `/api/v1/auth/login`, `/api/v1/auth/refresh`, `/api/v1/auth/logout` — без API key и без JWT
- `/api/v1/auth/me`, `/api/v1/auth/change-password` — только JWT
- `/api/v1/upload/*` — только JWT
- `/api/v1/upload/avatar/:userId` — JWT + роль `admin`
- `/api/v1/users/*` — сначала проходит outer middleware `/api/v1`, затем дополнительно требует JWT и роль `admin`
- Остальные `/api/v1/*` — принимают либо Bearer JWT, либо `X-API-Key`
## Роли
- `admin`
- `user`
## RBAC, найденный в backend
- `requireAdmin` реально используется только для:
- `/api/v1/users/*`
- `DELETE /api/v1/upload/avatar/:userId`
Предположение, подтвержденное кодом frontend и backend: обычный пользователь с валидным JWT может работать с маршрутами, остановками, расписаниями, праздниками и уведомлениями, потому что на этих ресурсах server-side role guard не применен.
## Rate limiting
- `/api/v1/auth/*``100` запросов в час
- `/api/v1/upload/*``50` запросов в час
- `/api/v1/*``1000` запросов в час
- При превышении возвращается `429 Too Many Requests`
Важно: в `backend/src/middleware/rateLimiter.js` есть заготовка Redis store, но она не подключена к `express-rate-limit`; по факту используется стандартный in-memory store библиотеки. Это влияет на реальное поведение в multi-instance production.
# Сущности домена
## Основные сущности
### Route
- Таблица: `routes`
- Поля: `id`, `route_number`, `name`, `type`, `color`, `is_active`, `description`, `created_at`, `updated_at`
- Enum `type`: `bus`, `minibus`, `trolleybus`, `tram`
### Stop
- Таблица: `stops`
- Поля: `id`, `name`, `location`, `address`, `description`, `is_active`, `created_at`, `updated_at`
- Дополнительные вычисляемые поля в API: `latitude`, `longitude`
- В list/detail некоторых запросов также возвращается raw-поле `location`; его точный формат зависит от сериализации PostGIS драйвером `pg`, поэтому для клиентов безопаснее использовать `latitude`/`longitude`
### RouteStop
- Таблица: `route_stops`
- Связь many-to-many между `routes` и `stops`
- Поля: `id`, `route_id`, `stop_id`, `sequence`, `time_offset_minutes`
### Schedule
- Таблица: `schedules`
- Поля: `id`, `route_id`, `day_type`, `departure_times`, `valid_from`, `valid_until`, `created_at`, `updated_at`
- Enum `day_type`: `weekday`, `saturday`, `sunday`, `holiday`
- `departure_times` хранится как массив `TIME[]`
### Holiday
- Таблица: `holidays`
- Поля: `id`, `date`, `name`, `is_recurring`, `recurring_month`, `recurring_day`, `created_at`
### Alert
- Таблица: `alerts`
- Поля: `id`, `route_id`, `alert_type`, `title`, `message`, `start_time`, `end_time`, `is_active`, `created_at`
- Enum `alert_type`: `delay`, `cancellation`, `detour`, `info`
- На read-endpoints дополнительно могут приходить `route_name`, `route_number`, `color`
### User
- Таблица: `users`
- Поля: `id`, `username`, `password_hash`, `full_name`, `email`, `role`, `is_active`, `last_login`, `created_at`, `updated_at`, `avatar_url`
### UserSession
- Таблица: `user_sessions`
- Поля: `id`, `user_id`, `refresh_token`, `expires_at`, `ip_address`, `user_agent`, `created_at`
### ApiKey
- Таблица: `api_keys`
- Поля: `id`, `key`, `app_name`, `rate_limit`, `is_active`, `created_at`, `last_used_at`
### HistoricalDelay
- Таблица: `historical_delays`
- Поля: `route_id`, `stop_id`, `hour_of_day`, `day_type`, `avg_delay_minutes`, `sample_count`
- Используется только в ETA
### Vehicle
- Таблица: `vehicles`
- Поля: `vehicle_number`, `registration`, `type`, `capacity`, `route_id`, `is_active`
- API endpoints не найдены
### SyncLog
- Таблица: `sync_log`
- Поля: `table_name`, `record_id`, `action`, `timestamp`
- Наполняется триггерами БД
## DTO / схемы, восстановленные по коду
### Auth DTO
- `LoginRequest`: `{ username: string, password: string }`
- `LoginResponse`: `{ user: UserLoginPayload, accessToken: string, refreshToken: string }`
- `RefreshRequest`: `{ refreshToken: string }`
- `RefreshResponse`: `{ accessToken: string }`
- `LogoutRequest`: `{ refreshToken?: string }`
- `ChangePasswordRequest`: `{ currentPassword: string, newPassword: string, refreshToken?: string }`
### Route DTO
- `CreateRouteRequest`: `{ route_number: string, name: string, type: RouteType, color?: string, description?: string }`
- `UpdateRouteRequest`: `{ route_number?: string, name?: string, type?: RouteType, color?: string, description?: string, is_active?: boolean }`
- `RouteSummary`: `Route + { stops_count }`
- `RouteDetail`: `Route + { stops: StopOnRoute[] }`
### Stop DTO
- `CreateStopRequest`: `{ name: string, latitude: number, longitude: number, address?: string, description?: string }`
- `UpdateStopRequest`: `{ name?: string, latitude?: number, longitude?: number, address?: string, description?: string, is_active?: boolean }`
- `StopSummary`: `Stop + { latitude, longitude, routes_count }`
- `StopDetail`: `Stop + { latitude, longitude, routes: RouteAtStop[] }`
### RouteStop DTO
- `AddRouteStopRequest`: `{ stopId: number, sequence?: number, timeOffsetMinutes?: number }`
- `UpdateRouteStopRequest`: `{ sequence?: number, timeOffsetMinutes?: number }`
- `ReorderRouteStopsRequest`: `{ stops: Array<{ stopId: number, sequence: number, timeOffsetMinutes?: number }> }`
### Schedule DTO
- `CreateScheduleRequest`: `{ routeId: number, dayType: ScheduleDayType, departureTimes: string[], validFrom?: string|null, validUntil?: string|null }`
- `UpdateScheduleRequest`: `{ departureTimes?: string[], validFrom?: string|null, validUntil?: string|null }`
- `CopyScheduleRequest`: `{ fromDayType: ScheduleDayType, toDayType: ScheduleDayType }`
### Holiday DTO
- `CreateHolidayRequest`: `{ date: string, name: string, isRecurring?: boolean }`
- `UpdateHolidayRequest`: `{ date?: string, name?: string, isRecurring?: boolean }`
### Alert DTO
- `CreateAlertRequest`: `{ route_id?: number|null, alert_type: AlertType, title: string, message: string, start_time: string, end_time?: string|null, is_active?: boolean }`
- `UpdateAlertRequest`: те же поля, все опциональны
### User DTO
- `CreateUserRequest`: `{ username: string, password: string, full_name?: string, email?: string, role?: 'admin'|'user' }`
- `UpdateUserRequest`: `{ full_name?: string, email?: string, role?: 'admin'|'user', is_active?: boolean }`
- `ResetPasswordRequest`: `{ newPassword: string }`
### Upload DTO
- `AvatarUploadRequest`: multipart form-data, поле `avatar`
- `AvatarUploadResponse`: `{ message: string, avatar_url: string, user: { id, username, avatar_url } }`
### ETA DTO
- `CalculateEtaRequest`: `{ stopId: number, routeId: number }`
- `CalculateEtaResponse`: `{ routeId, stopId, currentTime, dayType, arrivals: Arrival[] }`
- `Arrival`: `{ scheduledTime: string, estimatedTime: string, minutesUntil: number, delay: number }`
### Sync DTO
- `FullSyncResponse`: `{ syncType: 'full', timestamp, version, data, metadata }`
- `IncrementalSyncResponse`: `{ syncType: 'incremental', timestamp, lastSync, changes, alerts, metadata }`
## Связи предметной области
- `Route 1 -> N Schedule`
- `Route N <-> N Stop` через `RouteStop`
- `RouteStop.sequence` определяет фактический порядок остановок в маршруте
- `RouteStop.time_offset_minutes` связывает расписание отправления маршрута с расчетным временем прибытия на конкретную остановку
- `HistoricalDelay` привязан к `(route, stop, hour_of_day, day_type)` и корректирует ETA
- `Alert N -> 0..1 Route`
- `Vehicle N -> 0..1 Route` в схеме БД
- `User 1 -> N UserSession`
- `SyncLog` отслеживает изменения `routes`, `stops`, `route_stops`, `schedules`, `holidays`
## Что отсутствует в модели относительно исходного предположения
- `рейсы / trips` — не найдены как отдельная сущность
- `водители / drivers` — не найдены
- `диспетчеры / dispatchers` — не найдены как отдельная сущность
- `GPS / телеметрия` — не найдены
- `статусы рейсов` — не найдены
- `отчетность` — не найдены API/endpoints
# API-документация
## System
### [GET] /health
- Описание: health check приложения
- Авторизация: не требуется
- Параметры path: нет
- Query-параметры: нет
- Headers: нет обязательных
- Request Body: нет
- Response 200/201: `{ status, timestamp, version }`
- Ошибки: стандартный `404`/`500` для неверного маршрута или runtime error, но в коде endpoint всегда отвечает `200`
- Связанные сущности: нет
- Где найдено в коде: `backend/src/index.js` -> inline handler `app.get('/health', ...)`
## Auth
### [POST] /api/v1/auth/login
- Описание: логин оператора/администратора, выдает access/refresh tokens
- Авторизация: не требуется
- Параметры path: нет
- Query-параметры: нет
- Headers: `Content-Type: application/json`
- Request Body: `LoginRequest`
- Response 200/201: `LoginResponse`; `user` возвращается без `password_hash`
- Ошибки:
- `400` — нет `username` или `password`
- `401` — неверные credentials или пользователь неактивен/не найден
- `429` — превышен rate limit auth
- `500` — внутренняя ошибка
- Связанные сущности: `User`, `UserSession`
- Где найдено в коде: `backend/src/routes/auth.js` -> `authController.login`, `backend/src/controllers/authController.js#login`
- Пример запроса:
```json
{
"username": "admin",
"password": "admin123"
}
```
- Пример ответа:
```json
{
"user": {
"id": 1,
"username": "admin",
"full_name": "Главный администратор",
"email": "admin@polotsk-transit.local",
"role": "admin",
"is_active": true,
"last_login": "2026-04-16T10:00:00.000Z",
"created_at": "2026-04-01T08:00:00.000Z",
"updated_at": "2026-04-16T10:00:00.000Z",
"avatar_url": null
},
"accessToken": "jwt-access-token",
"refreshToken": "jwt-refresh-token"
}
```
### [POST] /api/v1/auth/refresh
- Описание: обмен refresh token на новый access token
- Авторизация: не требуется
- Параметры path: нет
- Query-параметры: нет
- Headers: `Content-Type: application/json`
- Request Body: `RefreshRequest`
- Response 200/201: `{ accessToken }`
- Ошибки:
- `400` — не передан `refreshToken`
- `401` — invalid refresh token / session expired / user inactive
- `429` — превышен rate limit auth
- `500` — внутренняя ошибка
- Связанные сущности: `UserSession`, `User`
- Где найдено в коде: `backend/src/routes/auth.js` -> `authController.refresh`, `backend/src/controllers/authController.js#refresh`
### [POST] /api/v1/auth/logout
- Описание: удаляет запись refresh session по `refreshToken`
- Авторизация: не требуется
- Параметры path: нет
- Query-параметры: нет
- Headers: `Content-Type: application/json`
- Request Body: `{ refreshToken?: string }`
- Response 200/201: `{ message: 'Logged out successfully' }`
- Ошибки:
- `429` — превышен rate limit auth
- `500` — внутренняя ошибка
- Связанные сущности: `UserSession`
- Где найдено в коде: `backend/src/routes/auth.js` -> `authController.logout`, `backend/src/controllers/authController.js#logout`
### [GET] /api/v1/auth/me
- Описание: возвращает текущего пользователя по JWT
- Авторизация: JWT Bearer token
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`
- Request Body: нет
- Response 200/201: `{ id, username, full_name, email, role, avatar_url, last_login, created_at }`
- Ошибки:
- `401` — токен отсутствует
- `403` — токен невалиден или истек
- `404` — пользователь не найден
- `500` — внутренняя ошибка
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/auth.js` -> `authenticateToken` + `authController.me`, `backend/src/controllers/authController.js#me`
### [POST] /api/v1/auth/change-password
- Описание: меняет пароль текущего пользователя
- Авторизация: JWT Bearer token
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`, `Content-Type: application/json`
- Request Body: `ChangePasswordRequest`
- Response 200/201: `{ message: 'Password changed successfully' }`
- Ошибки:
- `400` — нет `currentPassword`/`newPassword`, либо новый пароль короче 6 символов
- `401` — текущий пароль неверный
- `403` — токен невалиден/истек
- `404` — пользователь не найден
- `500` — внутренняя ошибка
- Связанные сущности: `User`, `UserSession`
- Где найдено в коде: `backend/src/routes/auth.js` -> `authenticateToken` + `authController.changePassword`, `backend/src/controllers/authController.js#changePassword`
## Upload
### [POST] /api/v1/upload/avatar
- Описание: загружает или заменяет аватар текущего пользователя
- Авторизация: JWT Bearer token
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`, `Content-Type: multipart/form-data`
- Request Body: `AvatarUploadRequest` c файлом `avatar`
- Response 200/201: `AvatarUploadResponse`
- Ошибки:
- `400` — файл не передан, файл > 5MB, неверный mime/type
- `401` — токен отсутствует
- `403` — токен невалиден/истек
- `429` — превышен upload rate limit
- `500` — внутренняя ошибка
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/upload.js` -> `upload.single('avatar')` + `uploadController.uploadAvatar`, `backend/src/controllers/uploadController.js#uploadAvatar`, `backend/src/middleware/upload.js`
### [DELETE] /api/v1/upload/avatar
- Описание: удаляет аватар текущего пользователя
- Авторизация: JWT Bearer token
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`
- Request Body: нет
- Response 200/201: `{ message: 'Avatar deleted successfully' }`
- Ошибки:
- `401` — токен отсутствует
- `403` — токен невалиден/истек
- `404`у пользователя нет аватара
- `429` — превышен upload rate limit
- `500` — внутренняя ошибка
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/upload.js` -> `uploadController.deleteAvatar`, `backend/src/controllers/uploadController.js#deleteAvatar`
### [DELETE] /api/v1/upload/avatar/:userId
- Описание: админ удаляет аватар любого пользователя
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: `userId`
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`
- Request Body: нет
- Response 200/201: `{ message: 'Avatar deleted successfully' }`
- Ошибки:
- `401` — токен отсутствует
- `403` — токен невалиден/истек или нет admin роли
- `404` — пользователь не найден или у него нет аватара
- `429` — превышен upload rate limit
- `500` — внутренняя ошибка
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/upload.js` -> `requireAdmin` + `uploadController.deleteUserAvatar`, `backend/src/controllers/uploadController.js#deleteUserAvatar`
## Routes
### [GET] /api/v1/routes
- Описание: список активных маршрутов
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `RouteSummary[]`
- Ошибки:
- `401` — не передан ни JWT, ни `X-API-Key`
- `403` — токен/API key невалидны
- `429` — общий rate limit API
- `500``Failed to fetch routes`
- Связанные сущности: `Route`, `RouteStop`
- Где найдено в коде: `backend/src/routes/api.js` -> `routesController.getAllRoutes`, `backend/src/controllers/routesController.js#getAllRoutes`
### [GET] /api/v1/routes/:id
- Описание: маршрут с вложенным списком остановок в порядке следования
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `RouteDetail`
- Ошибки:
- `404` — маршрут не найден или неактивен
- `401`/`403`/`429`
- `500``Failed to fetch route`
- Связанные сущности: `Route`, `RouteStop`, `Stop`
- Где найдено в коде: `backend/src/routes/api.js` -> `routesController.getRouteById`, `backend/src/controllers/routesController.js#getRouteById`
### [POST] /api/v1/routes
- Описание: создает маршрут
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CreateRouteRequest`
- Response 200/201: созданный `Route`
- Ошибки:
- `400` — отсутствуют `route_number`, `name` или `type`
- `401`/`403`/`429`
- `500``Failed to create route`; DB constraint violations также уйдут в `500`, отдельного `409` нет
- Связанные сущности: `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `routesController.createRoute`, `backend/src/controllers/routesController.js#createRoute`
- Пример запроса:
```json
{
"route_number": "1",
"name": "Центр - Вокзал",
"type": "bus",
"color": "#0066CC",
"description": "Основной маршрут через центр"
}
```
### [PUT] /api/v1/routes/:id
- Описание: частично обновляет маршрут
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `UpdateRouteRequest`
- Response 200/201: обновленный `Route`
- Ошибки:
- `404` — маршрут не найден
- `401`/`403`/`429`
- `500``Failed to update route`
- Связанные сущности: `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `routesController.updateRoute`, `backend/src/controllers/routesController.js#updateRoute`
### [DELETE] /api/v1/routes/:id
- Описание: удаляет маршрут
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Route deleted successfully' }`
- Ошибки:
- `404` — маршрут не найден
- `401`/`403`/`429`
- `500``Failed to delete route`
- Связанные сущности: `Route`, каскадно `RouteStop`, `Schedule`, `HistoricalDelay`, `Alert(route_id)`
- Где найдено в коде: `backend/src/routes/api.js` -> `routesController.deleteRoute`, `backend/src/controllers/routesController.js#deleteRoute`
## Route Stops
### [GET] /api/v1/routes/:routeId/stops
- Описание: ordered stop list для маршрута
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `RouteStop[]` в обогащенном виде `{ route_stop_id, sequence, time_offset_minutes, stop_id, name, address, latitude, longitude }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch route stops`
- Связанные сущности: `RouteStop`, `Stop`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `routeStopsController.getRouteStops`, `backend/src/controllers/routeStopsController.js#getRouteStops`
### [GET] /api/v1/routes/:routeId/available-stops
- Описание: активные остановки, еще не входящие в маршрут
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: массив `{ id, name, address, latitude, longitude }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch available stops`
- Связанные сущности: `Route`, `Stop`, `RouteStop`
- Где найдено в коде: `backend/src/routes/api.js` -> `routeStopsController.getAvailableStops`, `backend/src/controllers/routeStopsController.js#getAvailableStops`
### [POST] /api/v1/routes/:routeId/stops
- Описание: добавляет остановку в маршрут
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `AddRouteStopRequest`
- Response 200/201: raw запись `route_stops`
- Ошибки:
- `400` — нет `stopId`
- `404` — маршрут или остановка не найдены
- `401`/`403`/`429`
- `500``Failed to add stop to route`
- Связанные сущности: `RouteStop`, `Route`, `Stop`
- Где найдено в коде: `backend/src/routes/api.js` -> `routeStopsController.addStopToRoute`, `backend/src/controllers/routeStopsController.js#addStopToRoute`
### [PUT] /api/v1/routes/:routeId/stops/:routeStopId
- Описание: меняет позицию остановки в маршруте и/или `time_offset_minutes`
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`, `routeStopId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `UpdateRouteStopRequest`
- Response 200/201: обновленная raw запись `route_stops`
- Ошибки:
- `404` — route stop не найден
- `401`/`403`/`429`
- `500``Failed to update route stop`
- Связанные сущности: `RouteStop`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `routeStopsController.updateRouteStop`, `backend/src/controllers/routeStopsController.js#updateRouteStop`
### [DELETE] /api/v1/routes/:routeId/stops/:routeStopId
- Описание: удаляет остановку из маршрута и переупорядочивает sequence
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`, `routeStopId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Stop removed from route successfully' }`
- Ошибки:
- `404` — route stop не найден
- `401`/`403`/`429`
- `500``Failed to remove stop from route`
- Связанные сущности: `RouteStop`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `routeStopsController.removeStopFromRoute`, `backend/src/controllers/routeStopsController.js#removeStopFromRoute`
### [PUT] /api/v1/routes/:routeId/stops-reorder
- Описание: bulk-reorder всех остановок маршрута
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `ReorderRouteStopsRequest`
- Response 200/201: `{ message: 'Route stops reordered successfully', count }`
- Ошибки:
- `400``stops` не массив или пустой
- `401`/`403`/`429`
- `500``Failed to reorder route stops`
- Связанные сущности: `RouteStop`, `Route`, `Stop`
- Где найдено в коде: `backend/src/routes/api.js` -> `routeStopsController.reorderRouteStops`, `backend/src/controllers/routeStopsController.js#reorderRouteStops`
## Schedules
### [GET] /api/v1/routes/:routeId/schedules
- Описание: все расписания маршрута по типам дней
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `Schedule[]` с дополнительными `route_number`, `route_name`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch schedules`
- Связанные сущности: `Schedule`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.getRouteSchedules`, `backend/src/controllers/schedulesController.js#getRouteSchedules`
### [GET] /api/v1/routes/:routeId/schedule/today
- Описание: возвращает действующее расписание на дату, с учетом SQL-функции `get_schedule_type()` и праздников
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: `date` (опционально, `YYYY-MM-DD`)
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201:
- `{ date, scheduleType, isHoliday, holidayName, schedule }`
- если праздник и отдельного `holiday` расписания нет, endpoint fallback-ится на `sunday`
- Ошибки:
- `404` — расписание на нужный тип дня не найдено
- `401`/`403`/`429`
- `500``Failed to fetch schedule`
- Связанные сущности: `Schedule`, `Route`, `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.getTodaySchedule`, `backend/src/controllers/schedulesController.js#getTodaySchedule`
### [POST] /api/v1/routes/:routeId/schedules/copy
- Описание: копирует departure times из одного day type в другой
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `routeId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CopyScheduleRequest`
- Response 200/201: существующее обновленное либо новое `Schedule`
- Ошибки:
- `400` — отсутствуют `fromDayType`/`toDayType`
- `404` — source schedule не найден
- `401`/`403`/`429`
- `500``Failed to copy schedule`
- Связанные сущности: `Schedule`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.copySchedule`, `backend/src/controllers/schedulesController.js#copySchedule`
### [GET] /api/v1/schedules/:id
- Описание: чтение конкретного расписания
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `Schedule` с `route_number`, `route_name`
- Ошибки:
- `404` — расписание не найдено
- `401`/`403`/`429`
- `500``Failed to fetch schedule`
- Связанные сущности: `Schedule`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.getScheduleById`, `backend/src/controllers/schedulesController.js#getScheduleById`
### [POST] /api/v1/schedules
- Описание: создает расписание маршрута
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CreateScheduleRequest`
- Response 200/201: созданный `Schedule`
- Ошибки:
- `400` — нет `routeId`, `dayType` или `departureTimes`
- `400` — invalid `dayType`
- `400` — invalid departure time format, ожидается `HH:MM` или `HH:MM:SS`
- `409` — уже есть активное расписание на этот `routeId + dayType`
- `401`/`403`/`429`
- `500``Failed to create schedule`
- Связанные сущности: `Schedule`, `Route`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.createSchedule`, `backend/src/controllers/schedulesController.js#createSchedule`
### [PUT] /api/v1/schedules/:id
- Описание: обновляет departure times и/или validity window
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `UpdateScheduleRequest`
- Response 200/201: обновленный `Schedule`
- Ошибки:
- `404` — расписание не найдено
- `400` — invalid departure time format
- `401`/`403`/`429`
- `500``Failed to update schedule`
- Связанные сущности: `Schedule`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.updateSchedule`, `backend/src/controllers/schedulesController.js#updateSchedule`
### [DELETE] /api/v1/schedules/:id
- Описание: удаляет расписание
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Schedule deleted successfully' }`
- Ошибки:
- `404` — расписание не найдено
- `401`/`403`/`429`
- `500``Failed to delete schedule`
- Связанные сущности: `Schedule`
- Где найдено в коде: `backend/src/routes/api.js` -> `schedulesController.deleteSchedule`, `backend/src/controllers/schedulesController.js#deleteSchedule`
## Holidays
### [GET] /api/v1/holidays
- Описание: список праздников; optional filter по году
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: `year` (опционально)
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201:
- без `year`: массив raw `Holiday`
- с `year`: массив raw `Holiday`, отсортированный так, чтобы recurring праздники шли по target year, но поле `date` для recurring остается исходным stored date
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch holidays`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.getAllHolidays`, `backend/src/controllers/holidaysController.js#getAllHolidays`
### [GET] /api/v1/holidays/upcoming
- Описание: ближайшие праздники относительно `CURRENT_DATE`
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: `limit` (опционально, по умолчанию `5`)
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: массив `{ id, name, next_date, is_recurring }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch upcoming holidays`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.getUpcomingHolidays`, `backend/src/controllers/holidaysController.js#getUpcomingHolidays`
### [GET] /api/v1/holidays/year/:year
- Описание: праздники конкретного года с resolved recurring dates
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `year`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: массив `{ id, date, name, is_recurring }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch holidays for year`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.getHolidaysForYear`, `backend/src/controllers/holidaysController.js#getHolidaysForYear`
### [GET] /api/v1/holidays/check/:date
- Описание: проверка, является ли дата праздником
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `date` (`YYYY-MM-DD`)
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ date, isHoliday, holidayName }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to check holiday`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.checkHoliday`, `backend/src/controllers/holidaysController.js#checkHoliday`
### [POST] /api/v1/holidays
- Описание: создает праздник
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CreateHolidayRequest`
- Response 200/201: созданный/обновленный raw `Holiday`
- Ошибки:
- `400` — нет `date` или `name`
- `401`/`403`/`429`
- `500``Failed to create holiday`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.createHoliday`, `backend/src/controllers/holidaysController.js#createHoliday`
### [PUT] /api/v1/holidays/:id
- Описание: обновляет праздник
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `UpdateHolidayRequest`
- Response 200/201: обновленный raw `Holiday`
- Ошибки:
- `404` — праздник не найден
- `401`/`403`/`429`
- `500``Failed to update holiday`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.updateHoliday`, `backend/src/controllers/holidaysController.js#updateHoliday`
### [DELETE] /api/v1/holidays/:id
- Описание: удаляет праздник
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Holiday deleted successfully' }`
- Ошибки:
- `404` — праздник не найден
- `401`/`403`/`429`
- `500``Failed to delete holiday`
- Связанные сущности: `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `holidaysController.deleteHoliday`, `backend/src/controllers/holidaysController.js#deleteHoliday`
## Stops
### [GET] /api/v1/stops
- Описание: список активных остановок
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `StopSummary[]`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to fetch stops`
- Связанные сущности: `Stop`, `RouteStop`
- Где найдено в коде: `backend/src/routes/api.js` -> `stopsController.getAllStops`, `backend/src/controllers/stopsController.js#getAllStops`
### [GET] /api/v1/stops/nearby
- Описание: geospatial поиск остановок в радиусе
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры:
- `lat` — обязательно
- `lon` — обязательно
- `radius` — опционально, default `500`
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: массив остановок с `latitude`, `longitude`, `distance_meters`
- Ошибки:
- `400` — отсутствует `lat` или `lon`
- `401`/`403`/`429`
- `500``Failed to fetch nearby stops`
- Связанные сущности: `Stop`
- Где найдено в коде: `backend/src/routes/api.js` -> `stopsController.getNearbyStops`, `backend/src/controllers/stopsController.js#getNearbyStops`
### [GET] /api/v1/stops/:id
- Описание: остановка с маршрутами, проходящими через нее
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `StopDetail`
- Ошибки:
- `404` — остановка не найдена или неактивна
- `401`/`403`/`429`
- `500``Failed to fetch stop`
- Связанные сущности: `Stop`, `Route`, `RouteStop`
- Где найдено в коде: `backend/src/routes/api.js` -> `stopsController.getStopById`, `backend/src/controllers/stopsController.js#getStopById`
### [POST] /api/v1/stops
- Описание: создает остановку
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CreateStopRequest`
- Response 200/201: созданный raw `Stop` + `latitude` + `longitude`
- Ошибки:
- `400` — нет `name`, `latitude` или `longitude`
- `401`/`403`/`429`
- `500``Failed to create stop`
- Связанные сущности: `Stop`
- Где найдено в коде: `backend/src/routes/api.js` -> `stopsController.createStop`, `backend/src/controllers/stopsController.js#createStop`
### [PUT] /api/v1/stops/:id
- Описание: частично обновляет остановку
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `UpdateStopRequest`
- Response 200/201: обновленный raw `Stop` + `latitude` + `longitude`
- Ошибки:
- `404` — остановка не найдена
- `401`/`403`/`429`
- `500``Failed to update stop`
- Примечание: если body пустой, контроллер соберет невалидный SQL и фактически вернет `500`
- Связанные сущности: `Stop`
- Где найдено в коде: `backend/src/routes/api.js` -> `stopsController.updateStop`, `backend/src/controllers/stopsController.js#updateStop`
### [DELETE] /api/v1/stops/:id
- Описание: удаляет остановку
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Stop deleted successfully' }`
- Ошибки:
- `404` — остановка не найдена
- `401`/`403`/`429`
- `500``Failed to delete stop`
- Связанные сущности: `Stop`, каскадно `RouteStop`, `HistoricalDelay`
- Где найдено в коде: `backend/src/routes/api.js` -> `stopsController.deleteStop`, `backend/src/controllers/stopsController.js#deleteStop`
## ETA
### [POST] /api/v1/eta/calculate
- Описание: считает ETA для одной пары `routeId + stopId`
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CalculateEtaRequest`
- Response 200/201:
- если есть ближайшие рейсы: `{ routeId, stopId, currentTime, dayType, arrivals }`
- если на сегодня рейсов больше нет: `{ message: 'No more arrivals today', nextDay }`
- Ошибки:
- `400` — нет `stopId` или `routeId`
- `404` — нет расписания для текущего day type
- `404` — остановка не принадлежит маршруту
- `401`/`403`/`429`
- `500``Failed to calculate ETA`
- Связанные сущности: `Schedule`, `RouteStop`, `HistoricalDelay`, `Holiday`
- Где найдено в коде: `backend/src/routes/api.js` -> `etaController.calculateEta`, `backend/src/controllers/etaController.js#calculateEta`
### [GET] /api/v1/eta/stop/:stopId
- Описание: ETA по всем маршрутам для остановки
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `stopId`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ stopId, routes: Array<{ id, route_number, name, type, color, arrivals, error? }> }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to get stop ETAs`
- Связанные сущности: `Stop`, `Route`, `Schedule`, `RouteStop`, `HistoricalDelay`
- Где найдено в коде: `backend/src/routes/api.js` -> `etaController.getStopEtas`, `backend/src/controllers/etaController.js#getStopEtas`
- Важное отличие: helper `calculateEtaForRoute()` определяет day type по JS `Date#getDay()` и не использует SQL-функцию `get_schedule_type()`, поэтому праздничные даты здесь обрабатываются иначе, чем в `POST /eta/calculate`
## Sync
### [GET] /api/v1/sync
- Описание: full sync или incremental sync для offline-first клиентов
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: `lastSync` (опционально, ISO datetime)
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201:
- без `lastSync`: `FullSyncResponse`
- с `lastSync`: `IncrementalSyncResponse`
- Ошибки:
- `401`/`403`/`429`
- `500``Sync failed`
- Связанные сущности: `Route`, `Stop`, `RouteStop`, `Schedule`, `Alert`, `Holiday`, `SyncLog`
- Где найдено в коде: `backend/src/routes/api.js` -> `syncController.sync`, `backend/src/controllers/syncController.js#sync`
- Детали full sync:
- `data.routes`
- `data.stops`
- `data.routeStops`
- `data.schedules`
- `data.alerts` только активные
- `data.holidays`
- Детали incremental sync:
- `changes.routes.updated/deleted`
- `changes.stops.updated/deleted`
- `changes.routeStops.updated/deleted`
- `changes.schedules.updated/deleted`
- `alerts` — только новые активные alerts, созданные после `lastSync`
### [GET] /api/v1/sync/status
- Описание: статус системы синхронизации и агрегированные counts
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ status: 'ready', timestamp, database: { routes_count, stops_count, schedules_count, last_change } }`
- Ошибки:
- `401`/`403`/`429`
- `500``Failed to get sync status`
- Связанные сущности: `SyncLog`, `Route`, `Stop`, `Schedule`
- Где найдено в коде: `backend/src/routes/api.js` -> `syncController.getSyncStatus`, `backend/src/controllers/syncController.js#getSyncStatus`
## Alerts
### [GET] /api/v1/alerts
- Описание: список уведомлений с фильтрами
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры:
- `route_id` — опционально
- `is_active` — опционально, строка `'true'` или `'false'`
- `alert_type` — опционально, `delay|cancellation|detour|info`
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `Alert[]` с join-полями `route_name`, `route_number`, `color`
- Ошибки:
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`, `Route`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.get('/')`
### [GET] /api/v1/alerts/active
- Описание: только активные и актуальные по времени уведомления
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: `route_id` (опционально)
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `Alert[]` с join-полями маршрута
- Ошибки:
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`, `Route`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.get('/active')`
### [GET] /api/v1/alerts/:id
- Описание: получение одного уведомления
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: один `Alert` с join-полями маршрута
- Ошибки:
- `404``Alert not found`
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`, `Route`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.get('/:id')`
### [POST] /api/v1/alerts
- Описание: создает уведомление
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `CreateAlertRequest`
- Response 200/201: raw `Alert` без join-полей
- Ошибки:
- `400` — отсутствуют обязательные поля
- `400` — invalid `alert_type`
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`, `Route`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.post('/')`
### [PUT] /api/v1/alerts/:id
- Описание: обновляет уведомление
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body: `UpdateAlertRequest`
- Response 200/201: raw обновленный `Alert`
- Ошибки:
- `404``Alert not found`
- `400` — invalid `alert_type`
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`, `Route`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.put('/:id')`
### [DELETE] /api/v1/alerts/:id
- Описание: удаляет уведомление
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Alert deleted successfully' }`
- Ошибки:
- `404``Alert not found`
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.delete('/:id')`
### [POST] /api/v1/alerts/cleanup
- Описание: деактивирует истекшие уведомления
- Авторизация: Bearer JWT или `X-API-Key`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `{ message: 'Cleanup completed', deactivated_count }`
- Ошибки:
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `Alert`
- Где найдено в коде: `backend/src/routes/alerts.js` -> inline `router.post('/cleanup')`
## Users
### [GET] /api/v1/users
- Описание: список пользователей
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`
- Request Body: нет
- Response 200/201: массив `{ id, username, full_name, email, role, is_active, last_login, created_at }`
- Ошибки:
- `401` — нет/невалиден JWT
- `403` — нет admin роли
- `429` — общий rate limit API
- `500``Internal server error`
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/users.js` -> `authenticateToken` + `requireAdmin` + `usersController.getAllUsers`, `backend/src/controllers/usersController.js#getAllUsers`
### [GET] /api/v1/users/:id
- Описание: чтение одного пользователя
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`
- Request Body: нет
- Response 200/201: `{ id, username, full_name, email, role, is_active, last_login, created_at }`
- Ошибки:
- `401`/`403`/`429`
- `404``User not found`
- `500``Internal server error`
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/users.js` -> `usersController.getUserById`, `backend/src/controllers/usersController.js#getUserById`
### [POST] /api/v1/users
- Описание: создание пользователя
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`, `Content-Type: application/json`
- Request Body: `CreateUserRequest`
- Response 200/201: `{ id, username, full_name, email, role, is_active, created_at }`
- Ошибки:
- `400` — нет `username`/`password`
- `400` — пароль короче 6 символов
- `400` — username уже существует
- `401`/`403`/`429`
- `500``Internal server error`
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/users.js` -> `usersController.createUser`, `backend/src/controllers/usersController.js#createUser`
### [PUT] /api/v1/users/:id
- Описание: обновляет профиль, роль и активность пользователя
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`, `Content-Type: application/json`
- Request Body: `UpdateUserRequest`
- Response 200/201: `{ id, username, full_name, email, role, is_active }`
- Ошибки:
- `400` — нельзя деактивировать самого себя
- `401`/`403`/`429`
- `404``User not found`
- `500``Internal server error`
- Связанные сущности: `User`
- Где найдено в коде: `backend/src/routes/users.js` -> `usersController.updateUser`, `backend/src/controllers/usersController.js#updateUser`
### [DELETE] /api/v1/users/:id
- Описание: удаляет пользователя
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`
- Request Body: нет
- Response 200/201: `{ message: 'User deleted successfully' }`
- Ошибки:
- `400` — нельзя удалить самого себя
- `401`/`403`/`429`
- `404``User not found`
- `500``Internal server error`
- Связанные сущности: `User`, каскадно `UserSession`
- Где найдено в коде: `backend/src/routes/users.js` -> `usersController.deleteUser`, `backend/src/controllers/usersController.js#deleteUser`
### [POST] /api/v1/users/:id/reset-password
- Описание: сбрасывает пароль пользователя и инвалидирует все его сессии
- Авторизация: JWT Bearer token + роль `admin`
- Параметры path: `id`
- Query-параметры: нет
- Headers: `Authorization: Bearer <JWT>`, `Content-Type: application/json`
- Request Body: `ResetPasswordRequest`
- Response 200/201: `{ message: 'Password reset successfully' }`
- Ошибки:
- `400` — нет `newPassword` или пароль короче 6 символов
- `401`/`403`/`429`
- `404``User not found`
- `500``Internal server error`
- Связанные сущности: `User`, `UserSession`
- Где найдено в коде: `backend/src/routes/users.js` -> `usersController.resetPassword`, `backend/src/controllers/usersController.js#resetPassword`
## Telemetry / Realtime
### [POST] /api/v1/telemetry/ingest
- Описание: принимает GPS/телеметрию, при необходимости автоматически создает транспорт по `vehicleNumber`, обновляет `vehicle_live_state` и пушит событие в realtime-канал маршрута
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key: <key>`
- Параметры path: нет
- Query-параметры: нет
- Headers: `Authorization` или `X-API-Key`, `Content-Type: application/json`
- Request Body:
- `vehicleId?: number`
- `vehicleNumber?: string`
- `routeId?: number`
- `vehicleType?: 'bus' | 'minibus' | 'trolleybus' | 'tram'`
- `lat: number`
- `lon: number`
- `speedKmh?: number | null`
- `heading?: number | null`
- `accuracyMeters?: number | null`
- `recordedAt?: string (ISO datetime)`
- `sourceType?: string`
- `sourceRef?: string`
- Response 200/201:
- `{ message: 'Telemetry ingested successfully', vehicle: LiveVehicleState }`
- `vehicle` содержит:
- `vehicle_id`, `vehicle_number`, `registration`, `vehicle_type`, `capacity`
- `route_id`, `route_number`, `route_name`, `route_color`
- `latitude`, `longitude`, `speed_kmh`, `heading`, `accuracy_meters`
- `source_type`, `source_ref`, `last_seen_at`, `updated_at`
- `is_online`, `seconds_since_update`, `is_moving`
- Ошибки:
- `400` — некорректные `lat/lon`, `heading`, `recordedAt`, отсутствует `vehicleId|vehicleNumber`, `routeId` нужен для авто-создания транспорта
- `404``Route not found`, `Vehicle not found`
- `500``Failed to ingest telemetry`
- Связанные сущности: `vehicles`, `telemetry_samples`, `vehicle_live_state`, `routes`
- Где найдено в коде: `backend/src/routes/api.js` -> `telemetryController.ingest`, `backend/src/controllers/telemetryController.js#ingest`, `backend/src/services/telemetryService.js#ingestTelemetry`, `backend/migrations/005_realtime_gps.sql`
Пример запроса:
```json
{
"routeId": 7,
"vehicleNumber": "DEMO-7A",
"vehicleType": "bus",
"lat": 55.4869,
"lon": 28.7856,
"speedKmh": 24,
"heading": 90,
"accuracyMeters": 5,
"recordedAt": "2026-04-16T10:30:00.000Z"
}
```
### [GET] /api/v1/realtime/routes/:routeId/vehicles
- Описание: отдает snapshot живых транспортных единиц по маршруту
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key: <key>`
- Параметры path: `routeId`
- Query-параметры:
- `includeStale?: boolean` — включать offline/stale машины
- `staleAfterSeconds?: number` — окно online/offline, по умолчанию `180`
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `LiveVehicleState[]`
- Ошибки:
- `400``routeId must be a positive integer`
- `500``Failed to fetch route live vehicles`
- Связанные сущности: `vehicle_live_state`, `vehicles`, `routes`
- Где найдено в коде: `backend/src/routes/api.js` -> `telemetryController.getRouteLiveVehicles`, `backend/src/controllers/telemetryController.js#getRouteLiveVehicles`, `backend/src/services/telemetryService.js#getRouteLiveVehicles`
### [GET] /api/v1/realtime/vehicles/:vehicleId
- Описание: отдает последнюю известную live-позицию конкретной транспортной единицы
- Авторизация: `Authorization: Bearer <JWT>` или `X-API-Key: <key>`
- Параметры path: `vehicleId`
- Query-параметры:
- `staleAfterSeconds?: number` — окно online/offline, по умолчанию `180`
- Headers: `Authorization` или `X-API-Key`
- Request Body: нет
- Response 200/201: `LiveVehicleState`
- Ошибки:
- `400``vehicleId must be a positive integer`
- `404``Live state not found for this vehicle`
- `500``Failed to fetch vehicle live state`
- Связанные сущности: `vehicle_live_state`, `vehicles`, `routes`
- Где найдено в коде: `backend/src/routes/api.js` -> `telemetryController.getVehicleLiveState`, `backend/src/controllers/telemetryController.js#getVehicleLiveState`, `backend/src/services/telemetryService.js#getVehicleLiveState`
# WebSocket / Realtime API
Реализован WebSocket endpoint для подписки на живые GPS-обновления:
- Endpoint: `GET ws(s)://<host>/ws/realtime`
- Где найдено: `backend/src/services/realtimeService.js#attachRealtimeServer`, подключение в `backend/src/index.js#startServer`, proxy в `nginx/nginx.conf`
- Протокол: JSON messages поверх WebSocket
- Аутентификация: после подключения клиент обязан отправить `{ "type": "auth", "token": "<JWT>" }` или `{ "type": "auth", "apiKey": "<API key>" }`
Поддерживаемые входящие сообщения:
- `auth`
- `subscribe:route`
- `unsubscribe:route`
- `ping`
Поддерживаемые исходящие сообщения:
- `welcome`
- `auth:ok`
- `subscription:ok`
- `subscription:removed`
- `snapshot:route`
- `vehicle:position`
- `pong`
- `error`
Пример handshake:
```json
{ "type": "auth", "token": "<JWT>" }
```
```json
{ "type": "subscribe:route", "routeId": 7 }
```
Пример snapshot:
```json
{
"type": "snapshot:route",
"routeId": 7,
"vehicles": [
{
"vehicle_id": 12,
"vehicle_number": "DEMO-7A",
"route_id": 7,
"latitude": 55.4869,
"longitude": 28.7856,
"speed_kmh": 24,
"is_online": true,
"seconds_since_update": 3
}
]
}
```
# Внутренние интеграции
## PostgreSQL / PostGIS
- Все бизнес-данные хранятся в PostgreSQL
- Геооперации по остановкам выполняются через PostGIS `GEOGRAPHY(Point, 4326)`
- SQL-функции:
- `is_holiday(date)` — проверка праздничной даты
- `get_schedule_type(date)``weekday|saturday|sunday|holiday`
## Redis
- Кешируются:
- `routes:all`
- `route:{id}`
- `stops:all`
- При изменениях кэш инвалидируется в контроллерах
## Sync triggers
- Триггеры `log_sync_changes()` пишут изменения в `sync_log` для:
- `routes`
- `stops`
- `route_stops`
- `schedules`
- `holidays`
## Static media
- Публичная раздача файлов: `/uploads/*`
- Фактически используются аватары: `/uploads/avatars/<filename>`
- Путь примонтирован через `express.static(...)` в `backend/src/index.js`
## Frontend internal API layer
- `frontend/src/services/api.js` автоматически:
- добавляет Bearer JWT, если он есть
- иначе подставляет `X-API-Key`
- делает refresh access token при `401/403`
## Nginx
- Проксирует `/api/` на backend
- Проксирует `/` на frontend
- Включает свои rate limits
## Bootstrap scripts
- `backend/scripts/create-admin.js` создает пользователя `admin / admin123`
- Это operational helper, а не API endpoint
# Проблемы и неясности
## Подтвержденные проблемы
- Swagger/OpenAPI annotations в коде не найдены; auto-generated API docs отсутствуют
- GraphQL schema/resolvers не найдены
- Realtime теперь реализован через `ws`, но зависимость `socket.io` все еще лежит в `backend/package.json` и больше не соответствует фактической реализации
- `vehicles` есть в БД, но не имеет ни backend API, ни frontend UI
- `historical_delays` участвует в ETA, но CRUD/ingest API для наполнения этой таблицы не найден
- Инкрементальный sync не обрабатывает `holidays`, хотя на таблицу есть trigger `sync_holidays`
- Инкрементальный sync не обрабатывает обновления/удаления `alerts`; он возвращает только новые активные alerts после `lastSync`
- `GET /api/v1/eta/stop/:stopId` использует JS-логику day type и не учитывает SQL-функцию `get_schedule_type()`, поэтому поведение на праздниках расходится с `POST /api/v1/eta/calculate`
- `GET /api/v1/holidays?year=YYYY` сортирует recurring праздники под целевой год, но не резолвит поле `date`; корректно резолвит даты только `GET /api/v1/holidays/year/:year`
- `PUT /api/v1/stops/:id` при пустом body генерирует невалидный SQL и, по сути, сваливается в `500`
- В `backend/src/middleware/rateLimiter.js` реализован helper Redis store, но он не подключен в конфиг `rateLimit()`
- `backend/package.json` теперь содержит рабочий `src/utils/migrate.js`, но `src/utils/seed.js` все еще отсутствует
- Frontend ожидает `route.schedules_count` в `Dashboard.jsx` и `RoutesPage.jsx`, но backend `GET /api/v1/routes` это поле не возвращает
## Неясности и предположения
- Raw поле `location` в stop responses зависит от PostGIS/pg serialization; API фактически отдает его, но стабильный контракт по формату в коде не зафиксирован
- Агрегаты `COUNT(*)` и `COUNT(DISTINCT ...)` из PostgreSQL через `pg` часто приходят строками; frontend использует их без строгой типизации, поэтому клиентам лучше не полагаться на numeric JSON type без дополнительной нормализации
- Для create/update endpoints многие DB constraint violations не маппятся в `409/422`, а улетают в общий `500`
# Рекомендации
- Вынести этот восстановленный контракт в официальный `OpenAPI 3.x` и использовать его как single source of truth
- Добавить schema validation на входе (`zod`, `joi`, `express-validator`) и описать ошибки `400/422` детерминированно
- Явно разделить права API key и operator/admin JWT:
- read-only API key для публичных/mobile клиентов
- mutation endpoints только для JWT и соответствующих ролей
- Нормализовать stop responses: отдавать только `latitude/longitude`, а raw `location` убрать из внешнего API
- Доделать incremental sync для `holidays` и полного lifecycle `alerts`
- Привести ETA logic к одному источнику day-type truth: везде использовать SQL-функцию `get_schedule_type()`
- Либо добавить API для `vehicles`, telemetry, trip status и reporting, либо убрать эти сущности из предположений/README, чтобы документация совпадала с реальностью
- Исправить rate limiter так, чтобы Redis store реально использовался в production
- Исправить frontend env contract: использовать `VITE_API_URL` и согласовать `docker-compose.yml`
- Исправить backend `GET /routes`, если UI действительно нуждается в `schedules_count`
- Удалить или реализовать отсутствующие npm scripts `migrate` и `seed`
- Убрать default admin credentials из operational flow и заменить на безопасный bootstrap через env/CLI