Files
ridebus-admin/frontend/src/services/api.js
Zakhar Timoshenko 360b684019 feat: implement board device service with CRUD operations and validation
- Add boardDeviceService for managing device pairing, status, claiming, and revocation.
- Introduce boardDeviceSettings for default settings and validation logic.
- Create integration tests for device lifecycle, concurrent claims, and updates.
- Add unit tests for device pairing, claiming, and settings validation.
- Implement API tests for device endpoints with JWT authentication.
- Develop in-memory repository for testing purposes.
- Create frontend components for managing devices, including a form for device settings.
- Style DevicesPage with CSS for better user experience.
2026-10-10 00:47:26 +03:00

234 lines
8.5 KiB
JavaScript

import axios from 'axios';
import { getApiBaseUrl } from '../utils/apiBaseUrl';
const API_BASE_URL = getApiBaseUrl();
const API_KEY = import.meta.env.VITE_API_KEY || 'dev_key_12345678';
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Request interceptor for authentication
api.interceptors.request.use(
config => {
console.log(`API Request: ${config.method.toUpperCase()} ${config.url}`);
// Check if this is an auth endpoint
const isAuthEndpoint = config.url?.startsWith('/auth/');
const isPublicAuthEndpoint = config.url === '/auth/login' ||
config.url === '/auth/refresh' ||
config.url === '/auth/logout';
// For auth endpoints (except protected ones), don't add any auth headers
if (isAuthEndpoint && isPublicAuthEndpoint) {
return config;
}
// For all other endpoints, try to use JWT token first
const accessToken = localStorage.getItem('accessToken');
const activeCityId = localStorage.getItem('activeCityId');
if (activeCityId) {
config.headers['X-City-Id'] = activeCityId;
}
if (accessToken) {
// Use JWT token if available
config.headers.Authorization = `Bearer ${accessToken}`;
} else {
// Fall back to API key for non-authenticated requests
config.headers['X-API-Key'] = API_KEY;
}
return config;
},
error => {
console.error('API Request Error:', error);
return Promise.reject(error);
}
);
// Response interceptor for error handling and token refresh
api.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// If 401 or 403 and we have a refresh token, try to refresh
if ((error.response?.status === 401 || error.response?.status === 403) &&
!originalRequest._retry &&
localStorage.getItem('refreshToken')) {
originalRequest._retry = true;
try {
const refreshToken = localStorage.getItem('refreshToken');
const response = await axios.post(`${API_BASE_URL}/auth/refresh`, {
refreshToken
});
const { accessToken } = response.data;
localStorage.setItem('accessToken', accessToken);
// Retry original request with new token
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
} catch (refreshError) {
// Refresh failed, logout user
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
console.error('API Response Error:', error.response?.data || error.message);
return Promise.reject(error);
}
);
// Routes API
export const routesApi = {
getAll: () => api.get('/routes'),
getById: (id) => api.get(`/routes/${id}`),
create: (data) => api.post('/routes', data),
update: (id, data) => api.put(`/routes/${id}`, data),
delete: (id) => api.delete(`/routes/${id}`)
};
export const citiesApi = {
getAll: () => api.get('/cities'),
create: (data) => api.post('/cities', data),
update: (cityId, data) => api.put(`/cities/${cityId}`, data),
uploadAvatar: (cityId, file) => {
const formData = new FormData();
formData.append('avatar', file);
return api.post(`/cities/${cityId}/avatar`, formData, {
headers: { 'Content-Type': 'multipart/form-data' }
});
}
};
// Route Stops API (привязка остановок к маршрутам)
export const routeStopsApi = {
getRouteStops: (routeId) => api.get(`/routes/${routeId}/stops`),
getAvailableStops: (routeId) => api.get(`/routes/${routeId}/available-stops`),
addStop: (routeId, data) => api.post(`/routes/${routeId}/stops`, data),
updateStop: (routeId, routeStopId, data) => api.put(`/routes/${routeId}/stops/${routeStopId}`, data),
removeStop: (routeId, routeStopId) => api.delete(`/routes/${routeId}/stops/${routeStopId}`),
reorderStops: (routeId, stops) => api.put(`/routes/${routeId}/stops-reorder`, { stops })
};
// Stops API
export const stopsApi = {
getAll: () => api.get('/stops'),
getById: (id) => api.get(`/stops/${id}`),
getNearby: (lat, lon, radius = 500) =>
api.get('/stops/nearby', { params: { lat, lon, radius } }),
create: (data) => api.post('/stops', data),
update: (id, data) => api.put(`/stops/${id}`, data),
delete: (id) => api.delete(`/stops/${id}`)
};
// ETA API
export const etaApi = {
calculate: (stopId, routeId) =>
api.post('/eta/calculate', { stopId, routeId }),
getStopEtas: (stopId) => api.get(`/eta/stop/${stopId}`)
};
// Sync API
export const syncApi = {
sync: (lastSync) => api.get('/sync', { params: { lastSync } }),
getStatus: () => api.get('/sync/status')
};
export const qualityApi = {
getReport: () => api.get('/quality')
};
export const scheduleCenterApi = {
getOverview: (date) => api.get('/schedule-center', { params: { date } })
};
// Schedules API (расписания)
export const schedulesApi = {
getRouteSchedules: (routeId) => api.get(`/routes/${routeId}/schedules`),
getTodaySchedule: (routeId, date) => api.get(`/routes/${routeId}/schedule/today`, { params: { date } }),
getById: (id) => api.get(`/schedules/${id}`),
create: (data) => api.post('/schedules', data),
update: (id, data) => api.put(`/schedules/${id}`, data),
delete: (id) => api.delete(`/schedules/${id}`),
copy: (routeId, fromDayType, toDayType, sourceScheduleId) => api.post(`/routes/${routeId}/schedules/copy`, { fromDayType, toDayType, sourceScheduleId })
};
export const scheduleImportApi = {
catalog: () => api.get('/schedule-import/catalog'),
inspect: (data) => api.post('/schedule-import/inspect', data),
preview: (data) => api.post('/schedule-import/preview', data),
apply: (id) => api.post(`/schedule-import/${id}/apply`),
history: () => api.get('/schedule-import/history'),
news: () => api.get('/schedule-import/news'),
refreshNews: () => api.post('/schedule-import/news/refresh'),
reviewNews: (data) => api.post('/schedule-import/news/review', data),
};
// Holidays API (праздники)
export const holidaysApi = {
getAll: (year) => api.get('/holidays', { params: { year } }),
getUpcoming: (limit) => api.get('/holidays/upcoming', { params: { limit } }),
getForYear: (year) => api.get(`/holidays/year/${year}`),
checkDate: (date) => api.get(`/holidays/check/${date}`),
create: (data) => api.post('/holidays', data),
update: (id, data) => api.put(`/holidays/${id}`, data),
delete: (id) => api.delete(`/holidays/${id}`)
};
// Alerts API (уведомления)
export const telegramImportApi = {
getAll: () => api.get('/telegram-import'),
sync: () => api.post('/telegram-import/sync'),
review: (id, data) => api.post(`/telegram-import/${id}/review`, data),
getStopAliases: () => api.get('/telegram-import/stop-aliases'),
createStopAlias: data => api.post('/telegram-import/stop-aliases', data),
updateStopAlias: (id, data) => api.put(`/telegram-import/stop-aliases/${id}`, data),
deleteStopAlias: id => api.delete(`/telegram-import/stop-aliases/${id}`)
};
export const alertsApi = {
getAll: (params) => api.get('/alerts', { params }),
getActive: (routeId) => api.get('/alerts/active', { params: { route_id: routeId } }),
getTemplates: () => api.get('/alerts/templates'),
getById: (id) => api.get(`/alerts/${id}`),
create: (data, idempotencyKey) => api.post('/alerts', data, {
headers: idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : undefined
}),
update: (id, data) => api.put(`/alerts/${id}`, data),
delete: (id) => api.delete(`/alerts/${id}`),
cleanup: () => api.post('/alerts/cleanup')
};
// Telemetry API (живые GPS-точки)
export const telemetryApi = {
ingest: (data) => api.post('/telemetry/ingest', data)
};
// Realtime snapshot API (fallback/snapshot для live данных)
export const realtimeApi = {
getRouteVehicles: (routeId, params) => api.get(`/realtime/routes/${routeId}/vehicles`, { params }),
getVehicle: (vehicleId, params) => api.get(`/realtime/vehicles/${vehicleId}`, { params })
};
export const boardDevicesApi = {
list: () => api.get('/board-devices/admin'),
routes: stopId => api.get('/board-devices/admin/routes', { params: { stopId } }),
claim: data => api.post('/board-devices/admin/claim', data),
update: (id, data) => api.put(`/board-devices/admin/${id}`, data),
revoke: id => api.post(`/board-devices/admin/${id}/revoke`),
};
export default api;