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 поток запроса
- Nginx проксирует запросы на backend или frontend.
- Express применяет
helmet,cors,compression, body parsers и request logging. - Для
/api/v1/auth/*действует отдельный rate limit. - Для
/api/v1/upload/*действует JWT-аутентификация и отдельный rate limit. - Для
/api/v1/*действует middlewareauthenticateApiKey, который принимает либо JWT Bearer token, либоX-API-Key. - Контроллеры работают с PostgreSQL напрямую через
pg. - Отдельные read-маршруты и карточки сущностей кешируются в Redis.
- Изменения основных таблиц пишутся в
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 handlerbackend/src/routes/auth.js+backend/src/controllers/authController.js— JWT login/refresh/logout/profile/passwordbackend/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 routerv1backend/src/controllers/routesController.js— CRUD маршрутовbackend/src/controllers/stopsController.js— CRUD остановок и геопоиск через PostGISbackend/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 syncbackend/src/routes/alerts.js— CRUD уведомленийbackend/src/middleware/auth.js— JWT/API-key auth + admin guardbackend/src/middleware/rateLimiter.js— rate limitingbackend/src/middleware/upload.js— multer storage/filter/size limits
Frontend-модули
frontend/src/services/api.js— axios client, auth interceptors, API surfacefrontend/src/contexts/AuthContext.jsx— login, refresh, logout, profile bootstrapfrontend/src/components/ProtectedRoute.jsx— frontend RBAC guardfrontend/src/pages/*.jsx— операционные экраны для маршрутов, остановок, расписаний, праздников, уведомлений, пользователей и профиля
Инфраструктурные модули
backend/src/config/database.js— connection pool PostgreSQLbackend/src/config/redis.js— Redis clientbackend/src/config/logger.js— Winston loggingbackend/migrations/001_init.sql— основная схема БД и sync triggersbackend/migrations/002_holidays.sql— праздники Беларуси + SQL functionsis_holiday()иget_schedule_type()backend/migrations/003_users_auth.sql— пользователи и refresh sessionsbackend/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
Роли
adminuser
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, полеavatarAvatarUploadResponse:{ 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 ScheduleRoute N <-> N StopчерезRouteStopRouteStop.sequenceопределяет фактический порядок остановок в маршрутеRouteStop.time_offset_minutesсвязывает расписание отправления маршрута с расчетным временем прибытия на конкретную остановкуHistoricalDelayпривязан к(route, stop, hour_of_day, day_type)и корректирует ETAAlert N -> 0..1 RouteVehicle N -> 0..1 Routeв схеме БДUser 1 -> N UserSessionSyncLogотслеживает изменения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 handlerapp.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илиpassword401— неверные credentials или пользователь неактивен/не найден429— превышен rate limit auth500— внутренняя ошибка- Связанные сущности:
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— не переданrefreshToken401— invalid refresh token / session expired / user inactive429— превышен rate limit auth500— внутренняя ошибка- Связанные сущности:
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 auth500— внутренняя ошибка- Связанные сущности:
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:
AvatarUploadRequestc файломavatar - Response 200/201:
AvatarUploadResponse - Ошибки:
400— файл не передан, файл > 5MB, неверный mime/type401— токен отсутствует403— токен невалиден/истек429— превышен upload rate limit500— внутренняя ошибка- Связанные сущности:
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 limit500— внутренняя ошибка- Связанные сущности:
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 limit500— внутренняя ошибка- Связанные сущности:
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-Key403— токен/API key невалидны429— общий rate limit API500—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/429500—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илиtype401/403/429500—Failed 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/429500—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/429500—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/429500—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/429500—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— нетstopId404— маршрут или остановка не найдены401/403/429500—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/429500—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/429500—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/429500—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/429500—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/429500—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/toDayType404— source schedule не найден401/403/429500—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/429500—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илиdepartureTimes400— invaliddayType400— invalid departure time format, ожидаетсяHH:MMилиHH:MM:SS409— уже есть активное расписание на этотrouteId + dayType401/403/429500—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 format401/403/429500—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/429500—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: массив rawHoliday - с
year: массив rawHoliday, отсортированный так, чтобы recurring праздники шли по target year, но полеdateдля recurring остается исходным stored date - Ошибки:
401/403/429500—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/429500—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/429500—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/429500—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илиname401/403/429500—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/429500—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/429500—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/429500—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— опционально, default500- Headers:
AuthorizationилиX-API-Key - Request Body: нет
- Response 200/201: массив остановок с
latitude,longitude,distance_meters - Ошибки:
400— отсутствуетlatилиlon401/403/429500—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/429500—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илиlongitude401/403/429500—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/429500—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/429500—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илиrouteId404— нет расписания для текущего day type404— остановка не принадлежит маршруту401/403/429500—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/429500—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 по JSDate#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/429500—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.routesdata.stopsdata.routeStopsdata.schedulesdata.alertsтолько активныеdata.holidays- Детали incremental sync:
changes.routes.updated/deletedchanges.stops.updated/deletedchanges.routeStops.updated/deletedchanges.schedules.updated/deletedalerts— только новые активные 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/429500—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/429500—Internal server error- Связанные сущности:
Alert,Route - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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/429500—Internal server error- Связанные сущности:
Alert,Route - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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 found401/403/429500—Internal server error- Связанные сущности:
Alert,Route - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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— invalidalert_type401/403/429500—Internal server error- Связанные сущности:
Alert,Route - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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 found400— invalidalert_type401/403/429500—Internal server error- Связанные сущности:
Alert,Route - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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 found401/403/429500—Internal server error- Связанные сущности:
Alert - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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/429500—Internal server error- Связанные сущности:
Alert - Где найдено в коде:
backend/src/routes/alerts.js-> inlinerouter.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— нет/невалиден JWT403— нет admin роли429— общий rate limit API500—Internal server error- Связанные сущности:
User - Где найдено в коде:
backend/src/routes/users.js->authenticateToken+requireAdmin+usersController.getAllUsers,backend/src/controllers/usersController.js#getAllUsers
[GET] /api/v1/users/:id
- Описание: чтение одного пользователя
- Авторизация: JWT Bearer token + роль
admin - Параметры path:
id - Query-параметры: нет
- Headers:
Authorization: Bearer <JWT> - Request Body: нет
- Response 200/201:
{ id, username, full_name, email, role, is_active, last_login, created_at } - Ошибки:
401/403/429404—User not found500—Internal server error- Связанные сущности:
User - Где найдено в коде:
backend/src/routes/users.js->usersController.getUserById,backend/src/controllers/usersController.js#getUserById
[POST] /api/v1/users
- Описание: создание пользователя
- Авторизация: JWT Bearer token + роль
admin - Параметры path: нет
- Query-параметры: нет
- Headers:
Authorization: Bearer <JWT>,Content-Type: application/json - Request Body:
CreateUserRequest - Response 200/201:
{ id, username, full_name, email, role, is_active, created_at } - Ошибки:
400— нетusername/password400— пароль короче 6 символов400— username уже существует401/403/429500—Internal server error- Связанные сущности:
User - Где найдено в коде:
backend/src/routes/users.js->usersController.createUser,backend/src/controllers/usersController.js#createUser
[PUT] /api/v1/users/:id
- Описание: обновляет профиль, роль и активность пользователя
- Авторизация: JWT Bearer token + роль
admin - Параметры path:
id - Query-параметры: нет
- Headers:
Authorization: Bearer <JWT>,Content-Type: application/json - Request Body:
UpdateUserRequest - Response 200/201:
{ id, username, full_name, email, role, is_active } - Ошибки:
400— нельзя деактивировать самого себя401/403/429404—User not found500—Internal server error- Связанные сущности:
User - Где найдено в коде:
backend/src/routes/users.js->usersController.updateUser,backend/src/controllers/usersController.js#updateUser
[DELETE] /api/v1/users/:id
- Описание: удаляет пользователя
- Авторизация: JWT Bearer token + роль
admin - Параметры path:
id - Query-параметры: нет
- Headers:
Authorization: Bearer <JWT> - Request Body: нет
- Response 200/201:
{ message: 'User deleted successfully' } - Ошибки:
400— нельзя удалить самого себя401/403/429404—User not found500—Internal server error- Связанные сущности:
User, каскадноUserSession - Где найдено в коде:
backend/src/routes/users.js->usersController.deleteUser,backend/src/controllers/usersController.js#deleteUser
[POST] /api/v1/users/:id/reset-password
- Описание: сбрасывает пароль пользователя и инвалидирует все его сессии
- Авторизация: JWT Bearer token + роль
admin - Параметры path:
id - Query-параметры: нет
- Headers:
Authorization: Bearer <JWT>,Content-Type: application/json - Request Body:
ResetPasswordRequest - Response 200/201:
{ message: 'Password reset successfully' } - Ошибки:
400— нетnewPasswordили пароль короче 6 символов401/403/429404—User not found500—Internal server error- Связанные сущности:
User,UserSession - Где найдено в коде:
backend/src/routes/users.js->usersController.resetPassword,backend/src/controllers/usersController.js#resetPassword
Telemetry / Realtime
[POST] /api/v1/telemetry/ingest
- Описание: принимает GPS/телеметрию, при необходимости автоматически создает транспорт по
vehicleNumber, обновляетvehicle_live_stateи пушит событие в realtime-канал маршрута - Авторизация:
Authorization: Bearer <JWT>илиX-API-Key: <key> - Параметры path: нет
- Query-параметры: нет
- Headers:
AuthorizationилиX-API-Key,Content-Type: application/json - Request Body:
vehicleId?: numbervehicleNumber?: stringrouteId?: numbervehicleType?: 'bus' | 'minibus' | 'trolleybus' | 'tram'lat: numberlon: numberspeedKmh?: number | nullheading?: number | nullaccuracyMeters?: number | nullrecordedAt?: string (ISO datetime)sourceType?: stringsourceRef?: string- Response 200/201:
{ message: 'Telemetry ingested successfully', vehicle: LiveVehicleState }vehicleсодержит:vehicle_id,vehicle_number,registration,vehicle_type,capacityroute_id,route_number,route_name,route_colorlatitude,longitude,speed_kmh,heading,accuracy_meterssource_type,source_ref,last_seen_at,updated_atis_online,seconds_since_update,is_moving- Ошибки:
400— некорректныеlat/lon,heading,recordedAt, отсутствуетvehicleId|vehicleNumber,routeIdнужен для авто-создания транспорта404—Route not found,Vehicle not found500—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
Пример запроса:
{
"routeId": 7,
"vehicleNumber": "DEMO-7A",
"vehicleType": "bus",
"lat": 55.4869,
"lon": 28.7856,
"speedKmh": 24,
"heading": 90,
"accuracyMeters": 5,
"recordedAt": "2026-04-16T10:30:00.000Z"
}
[GET] /api/v1/realtime/routes/:routeId/vehicles
- Описание: отдает snapshot живых транспортных единиц по маршруту
- Авторизация:
Authorization: Bearer <JWT>илиX-API-Key: <key> - Параметры path:
routeId - Query-параметры:
includeStale?: boolean— включать offline/stale машиныstaleAfterSeconds?: number— окно online/offline, по умолчанию180- Headers:
AuthorizationилиX-API-Key - Request Body: нет
- Response 200/201:
LiveVehicleState[] - Ошибки:
400—routeId must be a positive integer500—Failed to fetch route live vehicles- Связанные сущности:
vehicle_live_state,vehicles,routes - Где найдено в коде:
backend/src/routes/api.js->telemetryController.getRouteLiveVehicles,backend/src/controllers/telemetryController.js#getRouteLiveVehicles,backend/src/services/telemetryService.js#getRouteLiveVehicles
[GET] /api/v1/realtime/vehicles/:vehicleId
- Описание: отдает последнюю известную live-позицию конкретной транспортной единицы
- Авторизация:
Authorization: Bearer <JWT>илиX-API-Key: <key> - Параметры path:
vehicleId - Query-параметры:
staleAfterSeconds?: number— окно online/offline, по умолчанию180- Headers:
AuthorizationилиX-API-Key - Request Body: нет
- Response 200/201:
LiveVehicleState - Ошибки:
400—vehicleId must be a positive integer404—Live state not found for this vehicle500—Failed to fetch vehicle live state- Связанные сущности:
vehicle_live_state,vehicles,routes - Где найдено в коде:
backend/src/routes/api.js->telemetryController.getVehicleLiveState,backend/src/controllers/telemetryController.js#getVehicleLiveState,backend/src/services/telemetryService.js#getVehicleLiveState
WebSocket / Realtime API
Реализован WebSocket endpoint для подписки на живые GPS-обновления:
- Endpoint:
GET ws(s)://<host>/ws/realtime - Где найдено:
backend/src/services/realtimeService.js#attachRealtimeServer, подключение вbackend/src/index.js#startServer, proxy вnginx/nginx.conf - Протокол: JSON messages поверх WebSocket
- Аутентификация: после подключения клиент обязан отправить
{ "type": "auth", "token": "<JWT>" }или{ "type": "auth", "apiKey": "<API key>" }
Поддерживаемые входящие сообщения:
authsubscribe:routeunsubscribe:routeping
Поддерживаемые исходящие сообщения:
welcomeauth:oksubscription:oksubscription:removedsnapshot:routevehicle:positionpongerror
Пример 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:allroute:{id}stops:all- При изменениях кэш инвалидируется в контроллерах
Sync triggers
- Триггеры
log_sync_changes()пишут изменения вsync_logдля: routesstopsroute_stopsschedulesholidays
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 UIhistorical_delaysучаствует в ETA, но CRUD/ingest API для наполнения этой таблицы не найден- Инкрементальный sync не обрабатывает
holidays, хотя на таблицу есть triggersync_holidays - Инкрементальный sync не обрабатывает обновления/удаления
alerts; он возвращает только новые активные alerts послеlastSync GET /api/v1/eta/stop/:stopIdиспользует JS-логику day type и не учитывает SQL-функциюget_schedule_type(), поэтому поведение на праздниках расходится сPOST /api/v1/eta/calculateGET /api/v1/holidays?year=YYYYсортирует recurring праздники под целевой год, но не резолвит полеdate; корректно резолвит даты толькоGET /api/v1/holidays/year/:yearPUT /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, но backendGET /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, а rawlocationубрать из внешнего API - Доделать incremental sync для
holidaysи полного lifecyclealerts - Привести 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