Files
MikrotikManager/shared/api/users.ts
T

148 lines
6.8 KiB
TypeScript

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 catalogPayload = z.object({ interfaces: z.array(catalogInterfaceSchema) })
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,
): Promise<CatalogIface[]> {
const payload = await requestJson<unknown>(
baseUrl,
`/api/users/interface-catalog?serverId=${encodeURIComponent(serverId)}`,
)
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 "Запрос не подтверждён."
}