feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,124 @@
|
||||
import { configuredBackendUrl } from "@/lib/backend-url"
|
||||
import {
|
||||
getToken,
|
||||
isAuthEnabled,
|
||||
redirectToPortalLogin,
|
||||
redirectToPortalLoginInteractive,
|
||||
} from "@/lib/auth"
|
||||
|
||||
export class ApiClientError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly status: number,
|
||||
public readonly payload?: unknown,
|
||||
) {
|
||||
super(message)
|
||||
this.name = "ApiClientError"
|
||||
}
|
||||
}
|
||||
|
||||
function trimBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.replace(/\/$/, "")
|
||||
}
|
||||
|
||||
/** Absolute or same-origin-relative URL for backend API paths. */
|
||||
export function resolveApiUrl(baseUrl: string, path: string): string {
|
||||
if (path.startsWith("/") && configuredBackendUrl().kind === "same-origin") {
|
||||
return path
|
||||
}
|
||||
// Safety: never call browser localhost when the UI is served from a remote host
|
||||
if (typeof window !== "undefined") {
|
||||
const host = window.location.hostname
|
||||
const remoteUi = host !== "localhost" && host !== "127.0.0.1"
|
||||
const baseIsLocal =
|
||||
/^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(trimBaseUrl(baseUrl))
|
||||
if (remoteUi && (baseIsLocal || !baseUrl.trim())) {
|
||||
return path.startsWith("/") ? path : `/${path}`
|
||||
}
|
||||
}
|
||||
return trimBaseUrl(baseUrl) + path
|
||||
}
|
||||
|
||||
/** Attach portal JWT when present. */
|
||||
export function withAuthHeaders(init?: HeadersInit): Headers {
|
||||
const headers = new Headers(init)
|
||||
const token = typeof window !== "undefined" ? getToken() : null
|
||||
if (token && !headers.has("Authorization")) {
|
||||
headers.set("Authorization", `Bearer ${token}`)
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
function handleUnauthorized(): never {
|
||||
if (typeof window !== "undefined" && isAuthEnabled()) {
|
||||
const ok = redirectToPortalLogin()
|
||||
if (!ok) redirectToPortalLoginInteractive()
|
||||
}
|
||||
throw new ApiClientError("Unauthorized", 401)
|
||||
}
|
||||
|
||||
async function parseErrorMessage(res: Response): Promise<string> {
|
||||
const payload = await res.json().catch(() => undefined)
|
||||
if (
|
||||
typeof payload === "object" &&
|
||||
payload !== null &&
|
||||
"error" in payload &&
|
||||
typeof (payload as { error?: unknown }).error === "string"
|
||||
) {
|
||||
return (payload as { error: string }).error
|
||||
}
|
||||
return res.statusText || `HTTP ${res.status}`
|
||||
}
|
||||
|
||||
export async function requestJson<T>(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<T> {
|
||||
const hasBody = init?.body != null
|
||||
const headers = withAuthHeaders(init?.headers)
|
||||
if (hasBody && !headers.has("Content-Type")) {
|
||||
headers.set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
const res = await fetch(resolveApiUrl(baseUrl, path), {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
|
||||
if (res.status === 401) handleUnauthorized()
|
||||
|
||||
if (res.status === 204) return undefined as T
|
||||
|
||||
const payload = await res.json().catch(() => undefined)
|
||||
if (!res.ok) {
|
||||
const msg =
|
||||
typeof payload === "object" &&
|
||||
payload !== null &&
|
||||
"error" in payload &&
|
||||
typeof (payload as { error?: unknown }).error === "string"
|
||||
? (payload as { error: string }).error
|
||||
: res.statusText
|
||||
throw new ApiClientError(msg, res.status, payload)
|
||||
}
|
||||
|
||||
return payload as T
|
||||
}
|
||||
|
||||
/** Binary/download endpoints (backup, backup file) with the same auth + URL rules. */
|
||||
export async function requestBlob(
|
||||
baseUrl: string,
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
const headers = withAuthHeaders(init?.headers)
|
||||
const res = await fetch(resolveApiUrl(baseUrl, path), {
|
||||
...init,
|
||||
headers,
|
||||
})
|
||||
if (res.status === 401) handleUnauthorized()
|
||||
if (!res.ok) {
|
||||
throw new ApiClientError(await parseErrorMessage(res), res.status)
|
||||
}
|
||||
return res
|
||||
}
|
||||
Reference in new issue
Block a user