Add CORS support and Docker configuration for web service
- Added CORS allowed origins in config.compose.yaml to enable cross-origin requests from the web service. - Updated docker-compose.yml to include a new web service with build context and port mapping for local development. - Enhanced gateway.go to support additional HTTP methods in CORS responses. - Updated README.md to document the new web service and its configuration requirements.
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/api/agg/summary": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Сводка флота и топы пользователей */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases?: components["parameters"]["aliases"];
|
||||
top_n?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AggEnvelopeSummary"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/agg/traffic": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Трафик по пользователям и серверам */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases?: components["parameters"]["aliases"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AggEnvelopeTrafficRows"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/agg/unique-ips": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Уникальные IP с привязкой к серверам (и опционально GeoIP) */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases?: components["parameters"]["aliases"];
|
||||
/** @description false — не обогащать GeoIP */
|
||||
geo?: "false" | "true";
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AggEnvelopeUniqueIPs"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/agg/users": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Список пользователей с merge по серверам */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases?: components["parameters"]["aliases"];
|
||||
include_links?: "true" | "false";
|
||||
min_total_megabytes?: number;
|
||||
min_total_octets?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AggEnvelopeUsersRows"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/agg/user/{username}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Один пользователь (тот же объект, что в users[]) */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases?: components["parameters"]["aliases"];
|
||||
include_links?: "true" | "false";
|
||||
};
|
||||
header?: never;
|
||||
path: {
|
||||
username: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AggEnvelopeUsersRow"];
|
||||
};
|
||||
};
|
||||
/** @description Пользователь не найден ни на одном upstream */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/agg/fleet-status": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Health + system/info по всем выбранным серверам */
|
||||
get: {
|
||||
parameters: {
|
||||
query?: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases?: components["parameters"]["aliases"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AggEnvelopeFleetStatus"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: {
|
||||
AggSuccessBase: {
|
||||
/** @enum {boolean} */
|
||||
ok: true;
|
||||
/** Format: date-time */
|
||||
generated_at: string;
|
||||
/** @description true если часть upstream недоступна */
|
||||
partial?: boolean;
|
||||
};
|
||||
AggEnvelopeSummary: components["schemas"]["AggSuccessBase"] & {
|
||||
/** @description SummaryData (см. реализацию / доку AGGREGATE.md) */
|
||||
data?: Record<string, never>;
|
||||
};
|
||||
AggEnvelopeTrafficRows: components["schemas"]["AggSuccessBase"] & {
|
||||
data?: components["schemas"]["TrafficRow"][];
|
||||
};
|
||||
AggEnvelopeUniqueIPs: components["schemas"]["AggSuccessBase"] & {
|
||||
data?: components["schemas"]["UniqueIPsRow"][];
|
||||
};
|
||||
AggEnvelopeUsersRows: components["schemas"]["AggSuccessBase"] & {
|
||||
data?: components["schemas"]["UsersRow"][];
|
||||
};
|
||||
AggEnvelopeUsersRow: components["schemas"]["AggSuccessBase"] & {
|
||||
data?: components["schemas"]["UsersRow"];
|
||||
};
|
||||
AggEnvelopeFleetStatus: components["schemas"]["AggSuccessBase"] & {
|
||||
data?: components["schemas"]["FleetStatusData"];
|
||||
};
|
||||
TrafficRow: {
|
||||
username?: string;
|
||||
servers?: {
|
||||
[key: string]: components["schemas"]["TrafficServerStats"];
|
||||
};
|
||||
};
|
||||
TrafficServerStats: {
|
||||
/** Format: float */
|
||||
total_megabytes?: number;
|
||||
/** Format: int64 */
|
||||
current_connections?: number;
|
||||
revision?: string;
|
||||
};
|
||||
UniqueIPsRow: {
|
||||
username?: string;
|
||||
ips?: components["schemas"]["IPAssignments"][];
|
||||
};
|
||||
IPAssignments: {
|
||||
ip?: string;
|
||||
active_on_servers?: string[];
|
||||
recent_on_servers?: string[];
|
||||
primary_server?: string | null;
|
||||
country_code?: string | null;
|
||||
country_name?: string | null;
|
||||
city_name?: string | null;
|
||||
/** Format: int64 */
|
||||
asn?: number | null;
|
||||
as_organization?: string | null;
|
||||
};
|
||||
UserLinks: {
|
||||
classic?: string[];
|
||||
secure?: string[];
|
||||
tls?: string[];
|
||||
};
|
||||
UsersRow: {
|
||||
username?: string;
|
||||
/** Format: float */
|
||||
total_megabytes?: number;
|
||||
by_server?: {
|
||||
[key: string]: components["schemas"]["TrafficServerStats"];
|
||||
};
|
||||
links?: components["schemas"]["UserLinks"];
|
||||
/** Format: int64 */
|
||||
active_unique_ips?: number;
|
||||
/** Format: int64 */
|
||||
recent_unique_ips?: number;
|
||||
user_ad_tag?: string | null;
|
||||
/** Format: int64 */
|
||||
max_tcp_conns?: number | null;
|
||||
expiration_rfc3339?: string | null;
|
||||
/** Format: int64 */
|
||||
data_quota_bytes?: number | null;
|
||||
/** Format: int64 */
|
||||
max_unique_ips?: number | null;
|
||||
};
|
||||
HealthData: {
|
||||
status?: string;
|
||||
read_only?: boolean;
|
||||
};
|
||||
SystemInfoData: {
|
||||
version?: string;
|
||||
target_arch?: string;
|
||||
target_os?: string;
|
||||
build_profile?: string;
|
||||
git_commit?: string | null;
|
||||
build_time_utc?: string | null;
|
||||
rustc_version?: string | null;
|
||||
/** Format: int64 */
|
||||
process_started_at_epoch_secs?: number;
|
||||
/** Format: float */
|
||||
uptime_seconds?: number;
|
||||
config_path?: string;
|
||||
config_hash?: string;
|
||||
/** Format: int64 */
|
||||
config_reload_count?: number;
|
||||
/** Format: int64 */
|
||||
last_config_reload_epoch_secs?: number | null;
|
||||
};
|
||||
FleetServerStatus: {
|
||||
alias?: string;
|
||||
ok?: boolean;
|
||||
health_ok?: boolean;
|
||||
health_http_status?: number;
|
||||
/** Format: int64 */
|
||||
health_latency_ms?: number;
|
||||
health_error?: string;
|
||||
health_revision?: string;
|
||||
health?: components["schemas"]["HealthData"];
|
||||
system_info_ok?: boolean;
|
||||
system_info_http_status?: number;
|
||||
/** Format: int64 */
|
||||
system_info_latency_ms?: number;
|
||||
system_info_error?: string;
|
||||
system_info_revision?: string;
|
||||
system_info?: components["schemas"]["SystemInfoData"];
|
||||
};
|
||||
FleetStatusData: {
|
||||
servers?: components["schemas"]["FleetServerStatus"][];
|
||||
servers_total?: number;
|
||||
servers_all_ok?: number;
|
||||
servers_failed?: number;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: {
|
||||
/** @description Список алиасов через запятую */
|
||||
aliases: string;
|
||||
};
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export type operations = Record<string, never>;
|
||||
@@ -0,0 +1,235 @@
|
||||
import { PUBLIC_TELEMT_GATEWAY_URL } from '$env/static/public';
|
||||
import type { components } from './aggregate.gen.js';
|
||||
import type { SummaryData } from './summary-types.js';
|
||||
import type {
|
||||
StatsSummaryData,
|
||||
TelemtErrorBody,
|
||||
TelemtSuccess,
|
||||
UserInfo
|
||||
} from './telemt-v1.js';
|
||||
|
||||
export function gatewayBase(): string {
|
||||
const u = PUBLIC_TELEMT_GATEWAY_URL || '';
|
||||
return u.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
export type AggEnvelope<T> = {
|
||||
ok: true;
|
||||
generated_at: string;
|
||||
partial?: boolean;
|
||||
data: T;
|
||||
};
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly status?: number,
|
||||
readonly body?: unknown
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
}
|
||||
}
|
||||
|
||||
async function parseJson(res: Response): Promise<unknown> {
|
||||
const text = await res.text();
|
||||
if (!text) return null;
|
||||
try {
|
||||
return JSON.parse(text);
|
||||
} catch {
|
||||
throw new ApiError('Ответ не JSON', res.status, text);
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAggSummary(params?: {
|
||||
aliases?: string;
|
||||
top_n?: number;
|
||||
}): Promise<AggEnvelope<SummaryData>> {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.aliases) q.set('aliases', params.aliases);
|
||||
if (params?.top_n != null) q.set('top_n', String(params.top_n));
|
||||
const url = `${gatewayBase()}/api/agg/summary${q.toString() ? `?${q}` : ''}`;
|
||||
const res = await fetch(url);
|
||||
const body = (await parseJson(res)) as Record<string, unknown> | null;
|
||||
if (!res.ok) {
|
||||
throw new ApiError(`summary HTTP ${res.status}`, res.status, body);
|
||||
}
|
||||
if (!body || body.ok !== true) {
|
||||
throw new ApiError('summary: ok !== true', res.status, body);
|
||||
}
|
||||
return body as AggEnvelope<SummaryData>;
|
||||
}
|
||||
|
||||
export async function fetchAggFleetStatus(params?: { aliases?: string }): Promise<
|
||||
AggEnvelope<components['schemas']['FleetStatusData']>
|
||||
> {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.aliases) q.set('aliases', params.aliases);
|
||||
const url = `${gatewayBase()}/api/agg/fleet-status${q.toString() ? `?${q}` : ''}`;
|
||||
const res = await fetch(url);
|
||||
const body = (await parseJson(res)) as Record<string, unknown> | null;
|
||||
if (!res.ok) throw new ApiError(`fleet-status HTTP ${res.status}`, res.status, body);
|
||||
if (!body || body.ok !== true) throw new ApiError('fleet-status: ok !== true', res.status, body);
|
||||
return body as AggEnvelope<components['schemas']['FleetStatusData']>;
|
||||
}
|
||||
|
||||
export async function fetchAggUniqueIps(params?: {
|
||||
aliases?: string;
|
||||
geo?: boolean;
|
||||
}): Promise<AggEnvelope<components['schemas']['UniqueIPsRow'][]>> {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.aliases) q.set('aliases', params.aliases);
|
||||
if (params?.geo === false) q.set('geo', 'false');
|
||||
const url = `${gatewayBase()}/api/agg/unique-ips${q.toString() ? `?${q}` : ''}`;
|
||||
const res = await fetch(url);
|
||||
const body = (await parseJson(res)) as Record<string, unknown> | null;
|
||||
if (!res.ok) throw new ApiError(`unique-ips HTTP ${res.status}`, res.status, body);
|
||||
if (!body || body.ok !== true) throw new ApiError('unique-ips: ok !== true', res.status, body);
|
||||
return body as AggEnvelope<components['schemas']['UniqueIPsRow'][]>;
|
||||
}
|
||||
|
||||
export async function fetchAggUsers(params?: {
|
||||
aliases?: string;
|
||||
include_links?: boolean;
|
||||
}): Promise<AggEnvelope<components['schemas']['UsersRow'][]>> {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.aliases) q.set('aliases', params.aliases);
|
||||
if (params?.include_links) q.set('include_links', 'true');
|
||||
const url = `${gatewayBase()}/api/agg/users${q.toString() ? `?${q}` : ''}`;
|
||||
const res = await fetch(url);
|
||||
const body = (await parseJson(res)) as Record<string, unknown> | null;
|
||||
if (!res.ok) throw new ApiError(`users HTTP ${res.status}`, res.status, body);
|
||||
if (!body || body.ok !== true) throw new ApiError('users: ok !== true', res.status, body);
|
||||
return body as AggEnvelope<components['schemas']['UsersRow'][]>;
|
||||
}
|
||||
|
||||
export async function fetchAggUser(
|
||||
username: string,
|
||||
params?: { aliases?: string; include_links?: boolean }
|
||||
): Promise<AggEnvelope<components['schemas']['UsersRow']>> {
|
||||
const q = new URLSearchParams();
|
||||
if (params?.aliases) q.set('aliases', params.aliases);
|
||||
if (params?.include_links) q.set('include_links', 'true');
|
||||
const path = encodeURIComponent(username);
|
||||
const url = `${gatewayBase()}/api/agg/user/${path}${q.toString() ? `?${q}` : ''}`;
|
||||
const res = await fetch(url);
|
||||
const body = (await parseJson(res)) as Record<string, unknown> | null;
|
||||
if (res.status === 404) throw new ApiError('Пользователь не найден', 404, body);
|
||||
if (!res.ok) throw new ApiError(`user HTTP ${res.status}`, res.status, body);
|
||||
if (!body || body.ok !== true) throw new ApiError('user: ok !== true', res.status, body);
|
||||
return body as AggEnvelope<components['schemas']['UsersRow']>;
|
||||
}
|
||||
|
||||
/** Путь к upstream без префикса /v1 — шлюз сам добавляет path_prefix. */
|
||||
function apiUrl(alias: string, path: string): string {
|
||||
const p = path.replace(/^\/+/, '');
|
||||
return `${gatewayBase()}/api/${encodeURIComponent(alias)}/${p}`;
|
||||
}
|
||||
|
||||
/** GET к Telemt через шлюз: путь без `/v1/` (например `health`, `stats/summary`). */
|
||||
export async function fetchTelemt<T>(alias: string, path: string): Promise<TelemtSuccess<T>> {
|
||||
const res = await fetch(apiUrl(alias, path));
|
||||
const body = await parseJson(res);
|
||||
if (!res.ok) {
|
||||
const err = body as TelemtErrorBody | null;
|
||||
const msg = err?.error?.message ?? `HTTP ${res.status}`;
|
||||
throw new ApiError(msg, res.status, body);
|
||||
}
|
||||
if (!body || (body as TelemtSuccess<T>).ok !== true) {
|
||||
throw new ApiError('Telemt: ok !== true', res.status, body);
|
||||
}
|
||||
return body as TelemtSuccess<T>;
|
||||
}
|
||||
|
||||
export async function fetchStatsSummary(alias: string): Promise<TelemtSuccess<StatsSummaryData>> {
|
||||
return fetchTelemt<StatsSummaryData>(alias, 'stats/summary');
|
||||
}
|
||||
|
||||
export async function fetchUsersList(alias: string): Promise<TelemtSuccess<UserInfo[]>> {
|
||||
return fetchTelemt<UserInfo[]>(alias, 'users');
|
||||
}
|
||||
|
||||
export async function fetchUserOne(alias: string, username: string): Promise<TelemtSuccess<UserInfo>> {
|
||||
const u = encodeURIComponent(username);
|
||||
return fetchTelemt<UserInfo>(alias, `users/${u}`);
|
||||
}
|
||||
|
||||
export async function createUser(
|
||||
alias: string,
|
||||
body: {
|
||||
username: string;
|
||||
secret?: string;
|
||||
user_ad_tag?: string;
|
||||
max_tcp_conns?: number;
|
||||
expiration_rfc3339?: string;
|
||||
data_quota_bytes?: number;
|
||||
max_unique_ips?: number;
|
||||
}
|
||||
): Promise<TelemtSuccess<{ user: UserInfo; secret: string }>> {
|
||||
const res = await fetch(apiUrl(alias, 'users'), {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const parsed = await parseJson(res);
|
||||
if (res.status !== 201) {
|
||||
const err = parsed as TelemtErrorBody | null;
|
||||
throw new ApiError(err?.error?.message ?? `HTTP ${res.status}`, res.status, parsed);
|
||||
}
|
||||
if (!parsed || (parsed as TelemtSuccess<{ user: UserInfo; secret: string }>).ok !== true) {
|
||||
throw new ApiError('create: ok !== true', res.status, parsed);
|
||||
}
|
||||
return parsed as TelemtSuccess<{ user: UserInfo; secret: string }>;
|
||||
}
|
||||
|
||||
export async function patchUser(
|
||||
alias: string,
|
||||
username: string,
|
||||
body: {
|
||||
secret?: string;
|
||||
user_ad_tag?: string;
|
||||
max_tcp_conns?: number;
|
||||
expiration_rfc3339?: string | null;
|
||||
data_quota_bytes?: number;
|
||||
max_unique_ips?: number;
|
||||
},
|
||||
ifMatch?: string | null
|
||||
): Promise<TelemtSuccess<UserInfo>> {
|
||||
const u = encodeURIComponent(username);
|
||||
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||
if (ifMatch) headers['If-Match'] = ifMatch;
|
||||
const res = await fetch(apiUrl(alias, `users/${u}`), {
|
||||
method: 'PATCH',
|
||||
headers,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const parsed = await parseJson(res);
|
||||
if (!res.ok) {
|
||||
const err = parsed as TelemtErrorBody | null;
|
||||
throw new ApiError(err?.error?.message ?? `HTTP ${res.status}`, res.status, parsed);
|
||||
}
|
||||
if (!parsed || (parsed as TelemtSuccess<UserInfo>).ok !== true) {
|
||||
throw new ApiError('patch: ok !== true', res.status, parsed);
|
||||
}
|
||||
return parsed as TelemtSuccess<UserInfo>;
|
||||
}
|
||||
|
||||
export async function deleteUser(
|
||||
alias: string,
|
||||
username: string,
|
||||
ifMatch?: string | null
|
||||
): Promise<TelemtSuccess<string>> {
|
||||
const u = encodeURIComponent(username);
|
||||
const headers: Record<string, string> = {};
|
||||
if (ifMatch) headers['If-Match'] = ifMatch;
|
||||
const res = await fetch(apiUrl(alias, `users/${u}`), { method: 'DELETE', headers });
|
||||
const parsed = await parseJson(res);
|
||||
if (!res.ok) {
|
||||
const err = parsed as TelemtErrorBody | null;
|
||||
throw new ApiError(err?.error?.message ?? `HTTP ${res.status}`, res.status, parsed);
|
||||
}
|
||||
if (!parsed || (parsed as TelemtSuccess<string>).ok !== true) {
|
||||
throw new ApiError('delete: ok !== true', res.status, parsed);
|
||||
}
|
||||
return parsed as TelemtSuccess<string>;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/** Соответствует internal/aggregate/types.go — SummaryData и опрос upstream. */
|
||||
export interface ServerFetchResultRow {
|
||||
alias: string;
|
||||
ok: boolean;
|
||||
http_status?: number;
|
||||
latency_ms?: number;
|
||||
error?: string;
|
||||
revision?: string;
|
||||
}
|
||||
|
||||
export interface TopUser {
|
||||
username: string;
|
||||
total_megabytes: number;
|
||||
}
|
||||
|
||||
export interface TopUserByUniqueIPs {
|
||||
username: string;
|
||||
unique_ips: number;
|
||||
}
|
||||
|
||||
export interface SummaryData {
|
||||
servers: ServerFetchResultRow[];
|
||||
servers_total: number;
|
||||
servers_ok: number;
|
||||
servers_failed: number;
|
||||
fleet_total_megabytes: number;
|
||||
fleet_total_connections: number;
|
||||
top_users: TopUser[];
|
||||
top_users_by_unique_ips: TopUserByUniqueIPs[];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/** Минимальные типы Telemt Control API (docs/API.md) для UI. */
|
||||
|
||||
export type TelemtSuccess<T> = { ok: true; data: T; revision?: string };
|
||||
export type TelemtErrorBody = {
|
||||
ok: false;
|
||||
error: { code: string; message: string };
|
||||
request_id?: number;
|
||||
};
|
||||
|
||||
export interface HealthData {
|
||||
status: string;
|
||||
read_only: boolean;
|
||||
}
|
||||
|
||||
export interface StatsSummaryData {
|
||||
uptime_seconds: number;
|
||||
connections_total: number;
|
||||
connections_bad_total: number;
|
||||
handshake_timeouts_total: number;
|
||||
configured_users: number;
|
||||
}
|
||||
|
||||
export interface UserLinks {
|
||||
classic?: string[];
|
||||
secure?: string[];
|
||||
tls?: string[];
|
||||
}
|
||||
|
||||
export interface UserInfo {
|
||||
username: string;
|
||||
user_ad_tag?: string | null;
|
||||
max_tcp_conns?: number | null;
|
||||
expiration_rfc3339?: string | null;
|
||||
data_quota_bytes?: number | null;
|
||||
max_unique_ips?: number | null;
|
||||
current_connections: number;
|
||||
active_unique_ips: number;
|
||||
active_unique_ips_list?: string[];
|
||||
recent_unique_ips: number;
|
||||
recent_unique_ips_list?: string[];
|
||||
total_octets: number;
|
||||
links?: UserLinks;
|
||||
}
|
||||
|
||||
export interface SystemInfoData {
|
||||
version: string;
|
||||
target_arch: string;
|
||||
target_os: string;
|
||||
build_profile: string;
|
||||
git_commit?: string | null;
|
||||
build_time_utc?: string | null;
|
||||
rustc_version?: string | null;
|
||||
process_started_at_epoch_secs: number;
|
||||
uptime_seconds: number;
|
||||
config_path: string;
|
||||
config_hash: string;
|
||||
config_reload_count: number;
|
||||
last_config_reload_epoch_secs?: number | null;
|
||||
}
|
||||
|
||||
export interface RuntimeGatesData {
|
||||
accepting_new_connections: boolean;
|
||||
conditional_cast_enabled: boolean;
|
||||
me_runtime_ready: boolean;
|
||||
me2dc_fallback_enabled: boolean;
|
||||
use_middle_proxy: boolean;
|
||||
startup_status: string;
|
||||
startup_stage: string;
|
||||
startup_progress_pct: number;
|
||||
}
|
||||
|
||||
export interface DcStatus {
|
||||
dc: number;
|
||||
rtt_ms?: number | null;
|
||||
alive_writers: number;
|
||||
required_writers: number;
|
||||
coverage_pct: number;
|
||||
load: number;
|
||||
}
|
||||
|
||||
export interface DcStatusData {
|
||||
middle_proxy_enabled: boolean;
|
||||
reason?: string | null;
|
||||
dcs?: DcStatus[];
|
||||
}
|
||||
|
||||
export interface RuntimeMePoolStateData {
|
||||
generations?: Record<string, unknown>;
|
||||
contour?: Record<string, unknown>;
|
||||
writers_health?: Record<string, unknown>;
|
||||
}
|
||||
Reference in New Issue
Block a user