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