improve api structure

This commit is contained in:
root 2025-10-01 22:15:26 +00:00
parent 29eb856a45
commit fb1c015d9a
4 changed files with 108 additions and 61 deletions

View File

@ -2,6 +2,16 @@
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
## Environment variables
Copy `.env.example` to `.env` (or provide the same variables through your deployment system) and adjust the URLs if needed. All frontend traffic (админка, загрузка, просмотр) завязан на `VITE_API_BASE_URL`, поэтому указывайте полный путь до `/api/v1` нужной ноды.
```
VITE_API_BASE_URL=https://my-public-node-8.projscale.dev/api/v1
```
При таком значении фронт автоматически отправляет tus-запросы на `https://my-public-node-8.projscale.dev/tus/files`, а прогрессивные загрузки (обложки, метаданные) — на `https://my-public-node-8.projscale.dev/api/v1.5/storage`.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh

View File

@ -1,18 +1,8 @@
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import axios, { InternalAxiosRequestConfig } from 'axios';
import { getAdminAuthSnapshot } from '~/shared/libs/admin-auth';
const API_BASE_PATH = '/api/v1';
const API_ENDPOINTS = [
'https://my-public-node-8.projscale.dev',
'http://localhost:3000',
] as const;
const API_BASE_URLS = API_ENDPOINTS.map((endpoint) => {
return `${endpoint.replace(/\/$/, '')}${API_BASE_PATH}`;
});
const ENDPOINT_INDEX_KEY = '__apiEndpointIndex__';
const isBrowser = typeof window !== 'undefined';
@ -28,10 +18,23 @@ const readStorage = (key: string) => {
}
};
type RequestConfig = InternalAxiosRequestConfig & {
[ENDPOINT_INDEX_KEY]?: number;
type RequestConfig = InternalAxiosRequestConfig;
const resolveDefaultBaseUrl = () => {
const envBase = (import.meta.env.VITE_API_BASE_URL ?? '').trim();
if (envBase) {
return envBase.replace(/\/$/, '');
}
if (isBrowser) {
return `${window.location.origin.replace(/\/$/, '')}${API_BASE_PATH}`;
}
return API_BASE_PATH;
};
const DEFAULT_API_BASE_URL = resolveDefaultBaseUrl();
export const request = axios.create({ withCredentials: true });
request.interceptors.request.use((config: RequestConfig) => {
@ -54,49 +57,11 @@ request.interceptors.request.use((config: RequestConfig) => {
}
}
const hasCustomBaseUrl = Boolean(config.baseURL && !API_BASE_URLS.includes(config.baseURL));
if (hasCustomBaseUrl) {
return config;
if (!config.baseURL) {
config.baseURL = DEFAULT_API_BASE_URL;
}
const attempt = typeof config[ENDPOINT_INDEX_KEY] === 'number' ? (config[ENDPOINT_INDEX_KEY] as number) : 0;
config[ENDPOINT_INDEX_KEY] = attempt;
config.baseURL = API_BASE_URLS[attempt] ?? API_BASE_URLS[0];
return config;
});
request.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const config = error.config as RequestConfig | undefined;
if (!config) {
return Promise.reject(error);
}
const hasCustomBaseUrl = Boolean(config.baseURL && !API_BASE_URLS.includes(config.baseURL));
if (hasCustomBaseUrl) {
return Promise.reject(error);
}
const isAbortError = error.code === 'ERR_CANCELED' || error.message === 'canceled';
const isNetworkError = !error.response && !isAbortError;
if (!isNetworkError) {
return Promise.reject(error);
}
const currentIndex = typeof config[ENDPOINT_INDEX_KEY] === 'number' ? (config[ENDPOINT_INDEX_KEY] as number) : 0;
const nextIndex = currentIndex + 1;
if (nextIndex >= API_BASE_URLS.length) {
return Promise.reject(error);
}
config[ENDPOINT_INDEX_KEY] = nextIndex;
config.baseURL = API_BASE_URLS[nextIndex];
return request(config);
},
);
request.interceptors.response.use((response) => response);

View File

@ -3,6 +3,15 @@ import { useMutation, useQuery } from "react-query";
import { request } from "~/shared/libs";
import { Royalty } from "~/shared/stores/root";
const VIEW_CONTENT_NODE_ORIGINS = [
"https://my-public-node-8.projscale.dev",
"https://my-public-node-7.projscale.dev",
"https://my-public-node-9.projscale.dev",
"https://my-public-node-10.projscale.dev",
"http://localhost:3000",
] as const;
const VIEW_CONTENT_API_PATH = "/api/v1";
type UseCreateNewContentPayload = {
title: string;
content: string;
@ -76,12 +85,34 @@ export const useCreateNewContent = () => {
export const useViewContent = (contentId: string | null | undefined) => {
return useQuery(
["view", "content", contentId],
() => {
return request.get(`/content.view/${contentId}`, {
headers: {
Authorization: localStorage.getItem('auth_v1_token') ?? ""
async () => {
if (!contentId) {
throw new Error("contentId is required");
}
let lastError: unknown = null;
for (const origin of VIEW_CONTENT_NODE_ORIGINS) {
const baseURL = `${origin.replace(/\/$/, "")}${VIEW_CONTENT_API_PATH}`;
try {
const response = await request.get(`/content.view/${contentId}`, {
baseURL,
});
if (origin !== VIEW_CONTENT_NODE_ORIGINS[0]) {
console.info("[App2Client][ContentView] Используем резервную ноду", {
origin,
});
}
});
return response;
} catch (error) {
lastError = error;
console.warn("[App2Client][ContentView] Нода недоступна", {
origin,
error,
});
}
}
throw lastError ?? new Error("All content nodes are unreachable");
},
{
enabled: Boolean(contentId),

View File

@ -4,9 +4,50 @@ import { Upload } from "tus-js-client";
import { request } from "~/shared/libs";
const STORAGE_API_URL = '/api/v1.5/storage';
const resolveApiBaseUrl = () => {
const envBase = (import.meta.env.VITE_API_BASE_URL ?? "").trim();
if (envBase) {
return envBase.replace(/\/$/, "");
}
if (typeof window !== "undefined") {
return `${window.location.origin.replace(/\/$/, "")}/api/v1`;
}
return "/api/v1";
};
const DEFAULT_API_BASE_URL = resolveApiBaseUrl();
const resolveUploadEndpoints = () => {
try {
const apiUrl = new URL(DEFAULT_API_BASE_URL);
const origin = apiUrl.origin.replace(/\/$/, "");
return {
storage: `${origin}/api/v1.5/storage`,
tus: `${origin}/tus/files`,
};
} catch (error) {
if (typeof window !== "undefined") {
const origin = window.location.origin.replace(/\/$/, "");
return {
storage: `${origin}/api/v1.5/storage`,
tus: `${origin}/tus/files`,
};
}
console.warn("[App2Client][Upload] Cannot derive upload endpoints from VITE_API_BASE_URL", {
error,
});
return {
storage: '/api/v1.5/storage',
tus: '/tus/files',
};
}
};
const { storage: STORAGE_API_URL, tus: TUS_ENDPOINT } = resolveUploadEndpoints();
const MAX_CHUNK_SIZE = 80 * 1024 * 1024; // 80 MB
const TUS_ENDPOINT = '/tus/files';
const TUS_STATUS_POLL_INTERVAL_MS = 2000;
const TUS_STATUS_POLL_TIMEOUT_MS = 5 * 60 * 1000;