feat(mcp): подготовить сервер и доступ к приёмке
This commit is contained in:
commit
bf3d50e1ff
3835 files changed
+537122
No files matched your search
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
interfaceCatalogStateSchema,
|
||||
type InterfaceCatalogState,
|
||||
appUserReadSchema,
|
||||
catalogInterfaceSchema,
|
||||
userBindingSchema,
|
||||
type AppUserCreate,
|
||||
type AppUserRead,
|
||||
type AppUserUpdate,
|
||||
type CatalogInterface,
|
||||
type UserBinding,
|
||||
type UserBindingCreate,
|
||||
} from "@mmapp/contracts/users"
|
||||
import { z } from "zod"
|
||||
import { requestJson, requestBlob } from "@/shared/api/http-client"
|
||||
import type { AppUser, CatalogIface, InterfaceBinding } from "@/lib/users"
|
||||
|
||||
const checkedUserSchema = appUserReadSchema.refine(user => user.bindings.every(binding => binding.userId === user.id) && new Set(user.bindings.map(binding => binding.id)).size === user.bindings.length, "Некорректный контекст привязок пользователя")
|
||||
|
||||
const usersListPayload = z.object({ users: z.array(checkedUserSchema).refine(rows => new Set(rows.map(row => row.id)).size === rows.length, "Повторяющиеся пользователи в ответе API") })
|
||||
const userPayload = z.object({ user: checkedUserSchema })
|
||||
const bindingPayload = z.object({ binding: userBindingSchema })
|
||||
const checkedCatalogInterface = catalogInterfaceSchema.refine(iface => iface.name.trim().length > 0 && (!(iface.type === "wg" || iface.type === "ipsec") || iface.peers !== undefined || !!iface.peersError) && (!iface.peers || iface.peers.every(peer => peer.publicKey.trim().length > 0) && new Set(iface.peers.map(peer => peer.publicKey)).size === iface.peers.length), "Неполный или неоднозначный каталог интерфейсов")
|
||||
const catalogPayload = z.object({ interfaces: z.array(checkedCatalogInterface).refine(rows => new Set(rows.map(row => row.name)).size === rows.length, "Повторяющиеся интерфейсы") })
|
||||
|
||||
export function toFrontendBinding(b: UserBinding): InterfaceBinding {
|
||||
return {
|
||||
id: b.id,
|
||||
userId: b.userId,
|
||||
serverId: String(b.serverId),
|
||||
serverName: b.serverName,
|
||||
serverSite: b.serverSite,
|
||||
serverCountry: b.serverCountry,
|
||||
interfaceName: b.interfaceName,
|
||||
interfaceType: b.interfaceType,
|
||||
peerPublicKey: b.peerPublicKey || undefined,
|
||||
peerName: b.peerName || undefined,
|
||||
comment: b.comment,
|
||||
}
|
||||
}
|
||||
|
||||
export function toFrontendUser(u: AppUserRead): AppUser {
|
||||
return {
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
login: u.login,
|
||||
email: u.email,
|
||||
role: u.role,
|
||||
last: u.lastSeen ?? "—",
|
||||
avatar: u.avatar,
|
||||
active: u.active,
|
||||
sections: u.sections,
|
||||
servers: u.servers,
|
||||
bindings: u.bindings.map(toFrontendBinding),
|
||||
}
|
||||
}
|
||||
|
||||
export async function listAppUsers(baseUrl: string, init?: RequestInit): Promise<AppUser[]> {
|
||||
const payload = await requestJson<unknown>(baseUrl, "/api/users", init)
|
||||
return usersListPayload.parse(payload).users.map(toFrontendUser)
|
||||
}
|
||||
|
||||
export async function getAppUser(baseUrl: string, id: string, init?: RequestInit): Promise<AppUser> {
|
||||
const payload = await requestJson<unknown>(baseUrl, "/api/users/" + encodeURIComponent(id), init)
|
||||
const user = toFrontendUser(userPayload.parse(payload).user)
|
||||
if (user.id !== id) throw new Error("Не подтверждён контекст пользователя.")
|
||||
return user
|
||||
}
|
||||
|
||||
function confirmProfile(user: AppUserRead, data: AppUserCreate | AppUserUpdate) {
|
||||
for (const key of ["name", "login", "email"] as const) {
|
||||
if (data[key] !== undefined && user[key] !== data[key]!.trim()) throw new Error("Профиль не подтверждён: ответ содержит другие поля.")
|
||||
}
|
||||
for (const key of ["role", "active"] as const) {
|
||||
if (data[key] !== undefined && user[key] !== data[key]) throw new Error("Профиль не подтверждён: ответ содержит другие права или состояние.")
|
||||
}
|
||||
const canonical = (items: unknown[]) => items.map(item => JSON.stringify(Object.entries(item as object).sort(([a], [b]) => a.localeCompare(b)))).sort().join("\n")
|
||||
for (const key of ["sections", "servers"] as const) {
|
||||
if (data[key] !== undefined && canonical(user[key]) !== canonical(data[key]!)) throw new Error("Профиль не подтверждён: разрешения отличаются от отправленных.")
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAppUser(baseUrl: string, data: AppUserCreate, init?: RequestInit): Promise<AppUser> {
|
||||
const payload = await requestJson<unknown>(baseUrl, "/api/users", {
|
||||
...init,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
const parsed = userPayload.parse(payload).user
|
||||
confirmProfile(parsed, data)
|
||||
const user = toFrontendUser(parsed)
|
||||
if (user.login !== data.login.trim()) throw new Error("Создание пользователя не подтверждено: другой логин.")
|
||||
return user
|
||||
}
|
||||
|
||||
export async function updateAppUser(baseUrl: string, id: string, data: AppUserUpdate, init?: RequestInit): Promise<AppUser> {
|
||||
const payload = await requestJson<unknown>(baseUrl, `/api/users/${encodeURIComponent(id)}`, {
|
||||
...init,
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
const parsed = userPayload.parse(payload).user
|
||||
confirmProfile(parsed, data)
|
||||
const user = toFrontendUser(parsed)
|
||||
if (user.id !== id) throw new Error("Сохранение пользователя не подтверждено: другой пользователь.")
|
||||
return user
|
||||
}
|
||||
|
||||
export async function deleteAppUser(baseUrl: string, id: string, init?: RequestInit): Promise<void> {
|
||||
const response = await requestBlob(baseUrl, "/api/users/" + encodeURIComponent(id), { ...init, method: "DELETE" })
|
||||
if (response.status !== 204) throw new Error("Удаление пользователя не подтверждено: ожидается HTTP 204.")
|
||||
}
|
||||
|
||||
export async function createUserBinding(
|
||||
baseUrl: string,
|
||||
userId: string,
|
||||
data: UserBindingCreate,
|
||||
init?: RequestInit,
|
||||
): Promise<InterfaceBinding> {
|
||||
const payload = await requestJson<unknown>(baseUrl, `/api/users/${encodeURIComponent(userId)}/bindings`, {
|
||||
...init,
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
const binding = toFrontendBinding(bindingPayload.parse(payload).binding)
|
||||
if (binding.userId !== userId || binding.serverId !== String(data.serverId) || binding.interfaceName !== data.interfaceName.trim() || (binding.peerPublicKey ?? "") !== (data.peerPublicKey ?? "").trim()) throw new Error("Привязка не подтверждена: ответ другого объекта.")
|
||||
return binding
|
||||
}
|
||||
|
||||
export async function deleteUserBinding(baseUrl: string, userId: string, bindingId: string, init?: RequestInit): Promise<void> {
|
||||
const response = await requestBlob(baseUrl, "/api/users/" + encodeURIComponent(userId) + "/bindings/" + encodeURIComponent(bindingId), { ...init, method: "DELETE" })
|
||||
if (response.status !== 204) throw new Error("Удаление привязки не подтверждено: ожидается HTTP 204.")
|
||||
}
|
||||
|
||||
export async function listInterfaceCatalog(
|
||||
baseUrl: string,
|
||||
serverId: string,
|
||||
init?: RequestInit,
|
||||
): Promise<CatalogIface[]> {
|
||||
const payload = await requestJson<unknown>(
|
||||
baseUrl,
|
||||
`/api/users/interface-catalog?serverId=${encodeURIComponent(serverId)}`,
|
||||
init,
|
||||
)
|
||||
return catalogPayload.parse(payload).interfaces as CatalogInterface[]
|
||||
}
|
||||
|
||||
export function userApiError(error: unknown): string {
|
||||
if (error instanceof z.ZodError) return "Некорректный ответ API. Данные не подтверждены."
|
||||
if (error instanceof Error) return error.message.length > 500 ? error.message.slice(0, 500) + "…" : error.message
|
||||
return "Запрос не подтверждён."
|
||||
}
|
||||
|
||||
export async function readInterfaceCatalogState(baseUrl: string, serverId: string, init?: RequestInit): Promise<InterfaceCatalogState> {
|
||||
const raw = await requestJson<unknown>(baseUrl, "/api/users/interface-catalog-state?serverId=" + encodeURIComponent(serverId), init)
|
||||
const state = interfaceCatalogStateSchema.parse(raw)
|
||||
if (String(state.serverId) !== serverId) throw new Error("Не подтверждён сервер каталога интерфейсов.")
|
||||
return state
|
||||
}
|
||||
Reference in new issue
Block a user