Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md). - 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой - Удалены legacy-views и переходный слой LegacyTabView (45 файлов) - Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки - CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf) - Фиксы: lazy-импорт именованных экспортов (краш после авторизации), растяжение #admin-root/#settings-root на всю ширину, возврат subjects/lessonTypes из справочников (краш просмотра расписаний), отказ от заведомо широкого поиска расписания без выбранной цели, центрирование карточки логина - Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину - Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
100 lines
2.7 KiB
JavaScript
100 lines
2.7 KiB
JavaScript
import { clearAuthState, fetchWithAuth } from '../../auth-session.js';
|
||
|
||
const DEFAULT_API_ERROR = 'Не удалось выполнить запрос';
|
||
const UNAUTHORIZED_ERROR = 'Требуется повторный вход в систему';
|
||
|
||
export class ApiRequestError extends Error {
|
||
constructor(message, { data = null, status = 0 } = {}) {
|
||
super(message);
|
||
this.name = 'ApiRequestError';
|
||
this.data = data;
|
||
this.status = status;
|
||
}
|
||
}
|
||
|
||
export async function requestJson(
|
||
endpoint,
|
||
{ errorMessage = DEFAULT_API_ERROR, headers: initialHeaders, json, ...options } = {},
|
||
{
|
||
fetchImpl = fetchWithAuth,
|
||
onUnauthorized = () => window.location.replace('/')
|
||
} = {}
|
||
) {
|
||
if (typeof endpoint !== 'string' || !endpoint.startsWith('/api/')) {
|
||
throw new ApiRequestError('Разрешены только same-origin запросы к API');
|
||
}
|
||
|
||
const headers = new Headers(initialHeaders || {});
|
||
headers.set('Accept', 'application/json');
|
||
|
||
let body = options.body;
|
||
if (json !== undefined) {
|
||
headers.set('Content-Type', 'application/json');
|
||
body = JSON.stringify(json);
|
||
}
|
||
|
||
const response = await fetchImpl(endpoint, {
|
||
...options,
|
||
body,
|
||
headers
|
||
});
|
||
const data = await readResponseData(response);
|
||
|
||
if (response.status === 401) {
|
||
clearAuthState();
|
||
onUnauthorized();
|
||
throw new ApiRequestError(UNAUTHORIZED_ERROR, { data, status: response.status });
|
||
}
|
||
|
||
if (!response.ok) {
|
||
throw new ApiRequestError(errorMessage, { data, status: response.status });
|
||
}
|
||
|
||
return data;
|
||
}
|
||
|
||
export const apiClient = Object.freeze({
|
||
delete: (endpoint, options = {}, dependencies) => requestJson(
|
||
endpoint,
|
||
{ ...options, method: 'DELETE' },
|
||
dependencies
|
||
),
|
||
get: (endpoint, options = {}, dependencies) => requestJson(
|
||
endpoint,
|
||
{ ...options, method: 'GET' },
|
||
dependencies
|
||
),
|
||
post: (endpoint, json, options = {}, dependencies) => requestJson(
|
||
endpoint,
|
||
{ ...options, json, method: 'POST' },
|
||
dependencies
|
||
),
|
||
put: (endpoint, json, options = {}, dependencies) => requestJson(
|
||
endpoint,
|
||
{ ...options, json, method: 'PUT' },
|
||
dependencies
|
||
)
|
||
});
|
||
|
||
async function readResponseData(response) {
|
||
if (response.status === 204) {
|
||
return null;
|
||
}
|
||
|
||
const text = await response.text();
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
|
||
const contentType = response.headers.get('Content-Type') || '';
|
||
if (!contentType.includes('application/json')) {
|
||
return text;
|
||
}
|
||
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|