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.

73 KiB

Обзор проекта

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
  • Пример запроса:
{
  "username": "admin",
  "password": "admin123"
}
  • Пример ответа:
{
  "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
  • 500Failed 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
  • 500Failed 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
  • 500Failed to create route; DB constraint violations также уйдут в 500, отдельного 409 нет
  • Связанные сущности: Route
  • Где найдено в коде: backend/src/routes/api.js -> routesController.createRoute, backend/src/controllers/routesController.js#createRoute
  • Пример запроса:
{
  "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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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 }
  • Ошибки:
  • 400stops не массив или пустой
  • 401/403/429
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Failed 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
  • 500Sync 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
  • 500Failed 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
  • 500Internal 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
  • 500Internal 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-полями маршрута
  • Ошибки:
  • 404Alert not found
  • 401/403/429
  • 500Internal 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
  • 500Internal 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
  • Ошибки:
  • 404Alert not found
  • 400 — invalid alert_type
  • 401/403/429
  • 500Internal 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' }
  • Ошибки:
  • 404Alert not found
  • 401/403/429
  • 500Internal 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
  • 500Internal 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
  • 500Internal 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
  • 404User not found
  • 500Internal 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
  • 500Internal 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
  • 404User not found
  • 500Internal 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
  • 404User not found
  • 500Internal 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
  • 404User not found
  • 500Internal 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 нужен для авто-создания транспорта
  • 404Route not found, Vehicle not found
  • 500Failed 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

Пример запроса:

{
  "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[]
  • Ошибки:
  • 400routeId must be a positive integer
  • 500Failed 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
  • Ошибки:
  • 400vehicleId must be a positive integer
  • 404Live state not found for this vehicle
  • 500Failed 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:

{ "type": "auth", "token": "<JWT>" }
{ "type": "subscribe:route", "routeId": 7 }

Пример snapshot:

{
  "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