The panel decides the probe/agent pairs and probes always dial out. A new fleet.auto_link setting connects every probe to every agent (default); with it off only the pairs chosen by hand connect, and any pair can be forced on or off. The Probes page has a link matrix with the switch and per-pair toggles.
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import { authHeaders, isRemote, resolveUrl } from './source'
|
|
|
|
/** 后端 API 客户端:默认同源请求(开发时由 Vite 代理到后端),Cookie 会话,修改类请求带 CSRF 令牌;指到别的后端时用 Bearer 令牌(见 source.ts)。 */
|
|
|
|
export class ApiError extends Error {
|
|
readonly status: number
|
|
readonly code: string
|
|
readonly detail: string
|
|
/** 字段级的校验错误:字段名 → 信息。 */
|
|
readonly errors: Record<string, string[]>
|
|
|
|
constructor(status: number, code: string, detail: string, errors: Record<string, string[]> = {}) {
|
|
super(detail || code)
|
|
this.status = status
|
|
this.code = code
|
|
this.detail = detail
|
|
this.errors = errors
|
|
}
|
|
}
|
|
|
|
let csrfToken: string | null = null
|
|
|
|
export function setCsrf(token: string | null) {
|
|
csrfToken = token
|
|
}
|
|
|
|
export type Query = Record<string, string | number | boolean | null | undefined>
|
|
|
|
export function withQuery(path: string, query?: Query): string {
|
|
if (!query) return path
|
|
const params = new URLSearchParams()
|
|
for (const [k, v] of Object.entries(query)) {
|
|
if (v !== undefined && v !== null && v !== '') params.set(k, String(v))
|
|
}
|
|
const text = params.toString()
|
|
return text ? `${path}?${text}` : path
|
|
}
|
|
|
|
async function request<T>(method: string, path: string, body?: unknown, init?: RequestInit): Promise<T> {
|
|
const headers: Record<string, string> = { Accept: 'application/json', ...authHeaders() }
|
|
if (body !== undefined) headers['Content-Type'] = method === 'PATCH' ? 'application/merge-patch+json' : 'application/json'
|
|
if (method !== 'GET' && csrfToken) headers['X-CSRF-Token'] = csrfToken
|
|
const response = await fetch(resolveUrl(path), {
|
|
method,
|
|
headers,
|
|
credentials: isRemote() ? 'omit' : 'include',
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
...init,
|
|
})
|
|
if (response.status === 204) return undefined as T
|
|
const text = await response.text()
|
|
const data = text ? (JSON.parse(text) as unknown) : undefined
|
|
if (!response.ok) {
|
|
const problem = (data ?? {}) as { code?: string; detail?: string; title?: string; errors?: Record<string, string[]> | null }
|
|
throw new ApiError(response.status, problem.code ?? 'error', problem.detail ?? problem.title ?? response.statusText, problem.errors ?? {})
|
|
}
|
|
return data as T
|
|
}
|
|
|
|
/**
|
|
* 在另一个后端上用用户名和密码登录:不设 Cookie(跨域用不了),把会话令牌放在响应里返回(`return_session`),
|
|
* 调用方存进浏览器,之后每个请求带上。
|
|
*/
|
|
export async function loginTo(base: string, username: string, password: string): Promise<string> {
|
|
const response = await fetch(`${base}/api/admin/auth/login`, {
|
|
method: 'POST',
|
|
headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
|
|
credentials: 'omit',
|
|
body: JSON.stringify({ username, password, return_session: true }),
|
|
})
|
|
const text = await response.text()
|
|
const data = text ? (JSON.parse(text) as unknown) : undefined
|
|
if (!response.ok) {
|
|
const problem = (data ?? {}) as { code?: string; detail?: string; title?: string }
|
|
throw new ApiError(response.status, problem.code ?? 'error', problem.detail ?? problem.title ?? response.statusText)
|
|
}
|
|
const token = (data as { session_token?: string | null } | undefined)?.session_token
|
|
if (!token) throw new ApiError(502, 'no_session', '这个后端的版本还不支持跨域登录,请先升级后端')
|
|
return token
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string, query?: Query, init?: RequestInit) => request<T>('GET', withQuery(path, query), undefined, init),
|
|
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body ?? {}),
|
|
patch: <T>(path: string, body: unknown) => request<T>('PATCH', path, body),
|
|
put: <T>(path: string, body: unknown) => request<T>('PUT', path, body),
|
|
del: <T>(path: string) => request<T>('DELETE', path),
|
|
}
|