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,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>;
|
||||
}
|
||||
Reference in New Issue
Block a user