# Обзор проекта `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 ` для операторов/админов админ-панели - 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 ` - 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 `, `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 `, `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 ` - 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 ` - 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 ` или `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 ` - 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 ` - 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 `, `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 `, `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 ` - 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 `, `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 ` или `X-API-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 ` или `X-API-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 ` или `X-API-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):///ws/realtime` - Где найдено: `backend/src/services/realtimeService.js#attachRealtimeServer`, подключение в `backend/src/index.js#startServer`, proxy в `nginx/nginx.conf` - Протокол: JSON messages поверх WebSocket - Аутентификация: после подключения клиент обязан отправить `{ "type": "auth", "token": "" }` или `{ "type": "auth", "apiKey": "" }` Поддерживаемые входящие сообщения: - `auth` - `subscribe:route` - `unsubscribe:route` - `ping` Поддерживаемые исходящие сообщения: - `welcome` - `auth:ok` - `subscription:ok` - `subscription:removed` - `snapshot:route` - `vehicle:position` - `pong` - `error` Пример handshake: ```json { "type": "auth", "token": "" } ``` ```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/` - Путь примонтирован через `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