import { 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 { const payload = await requestJson(baseUrl, "/api/users", init) return usersListPayload.parse(payload).users.map(toFrontendUser) } export async function getAppUser(baseUrl: string, id: string, init?: RequestInit): Promise { const payload = await requestJson(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 { const payload = await requestJson(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 { const payload = await requestJson(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 { 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 { const payload = await requestJson(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 { 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 { const payload = await requestJson( 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 "Запрос не подтверждён." }