import { ipsecListResponseSchema, ipsecServerSummaryDtoSchema, ipsecUserCreatedSchema, ipsecCertBundleSchema, ipsecExistingCertificateExportIntentSchema, ipsecExistingCertificateExportRequestKeySchema } from "@mmapp/contracts/ipsec" import { requestJson } from "@/shared/api/http-client" import { z } from "zod" import type { IpsecCertBundle, IpsecInitRequest, IpsecListResponse, IpsecPeerPatch, IpsecServerSummaryDto, IpsecUserCreateRequest, IpsecUserCreated, IpsecUserPatch, } from "@mmapp/contracts/ipsec" export async function listIpsec( baseUrl: string, opts?: { serverId?: string }, ): Promise { const q = new URLSearchParams() if (opts?.serverId) q.set("serverId", opts.serverId) const suffix = q.size > 0 ? `?${q.toString()}` : "" const response = await requestJson(baseUrl, `/api/ipsec${suffix}`, { method: "GET" }) const result = ipsecListResponseSchema.safeParse(response) if (!result.success) throw new Error("Некорректный ответ загрузки IPsec") const data = result.data const serverIds = data.servers.map(s => s.serverId) const clientIds = data.clients.map(c => c.id) const failedIds = (data.failures ?? []).map(f => f.serverId) if (new Set(serverIds).size !== serverIds.length || new Set(clientIds).size !== clientIds.length || new Set(failedIds).size !== failedIds.length || failedIds.some(id => !id.trim()) || data.servers.some(s => s.peers.some(p => p.serverId !== s.serverId) || new Set(s.peers.map(p => p.id)).size !== s.peers.length || [s.peer, s.pool, s.sharedModeConfig].some(child => child && child.serverId !== s.serverId) || new Set((s.certs ?? []).map(c => c.name)).size !== (s.certs ?? []).length) || serverIds.some(id => failedIds.includes(id)) || data.clients.some(c => failedIds.includes(c.serverId)) || (opts?.serverId && [...serverIds, ...failedIds, ...data.clients.map(c => c.serverId)].some(id => id !== opts.serverId))) { throw new Error("Несогласованный ответ загрузки IPsec") } return data } export async function initIpsecServer( baseUrl: string, body: IpsecInitRequest, ): Promise { const response = await requestJson(baseUrl, "/api/ipsec/server/init", { method: "POST", body: JSON.stringify(body), }) const result = ipsecServerSummaryDtoSchema.safeParse(response) if (!result.success || result.data.serverId !== body.serverId || !result.data.initialized) { throw new Error("Сервер не подтвердил инициализацию IKEv2") } return result.data } export async function deleteIpsecServer( baseUrl: string, serverId: string, opts?: { removeCertificates?: boolean }, ): Promise<{ ok: boolean }> { const q = new URLSearchParams() if (opts?.removeCertificates === false) q.set("removeCertificates", "false") const suffix = q.size > 0 ? `?${q.toString()}` : "" const result = await requestJson<{ ok: boolean }>( baseUrl, `/api/ipsec/server/${encodeURIComponent(serverId)}${suffix}`, { method: "DELETE" }, ) if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление") return result } export async function createIpsecUser( baseUrl: string, body: IpsecUserCreateRequest, ): Promise { const response = await requestJson(baseUrl, "/api/ipsec/users", { method: "POST", body: JSON.stringify(body), }) const result = ipsecUserCreatedSchema.safeParse(response) if (!result.success || result.data.client.serverId !== String(body.serverId) || result.data.client.name !== body.name.trim() || result.data.client.authMethod !== (body.authMethod ?? "certificate")) { throw new Error("Сервер не подтвердил создание клиента") } return result.data } export async function patchIpsecUser( baseUrl: string, serverId: string, clientId: string, patch: IpsecUserPatch, ): Promise<{ ok: boolean }> { const result = await requestJson<{ ok: boolean }>( baseUrl, `/api/ipsec/users/${encodeURIComponent(serverId)}/${encodeURIComponent(clientId)}`, { method: "PATCH", body: JSON.stringify(patch) }, ) if (result?.ok !== true) throw new Error("Сервер не подтвердил сохранение клиента") return result } export async function deleteIpsecUser( baseUrl: string, serverId: string, clientId: string, opts?: { removeCertificate?: boolean }, ): Promise<{ ok: boolean }> { const q = new URLSearchParams() if (opts?.removeCertificate === false) q.set("removeCertificate", "false") const suffix = q.size > 0 ? `?${q.toString()}` : "" const result = await requestJson<{ ok: boolean }>( baseUrl, `/api/ipsec/users/${encodeURIComponent(serverId)}/${encodeURIComponent(clientId)}${suffix}`, { method: "DELETE" }, ) if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление") return result } function parseExportBundle(response: unknown): IpsecCertBundle { const result = ipsecCertBundleSchema.safeParse(response) if (!result.success || !result.data.filename.trim() || !result.data.user.trim() || !result.data.passphrase || result.data.passphrase.length < 8 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(result.data.contentB64) || !result.data.contentB64) { throw new Error("Некорректный ответ экспорта сертификата") } return result.data } const existingCertExportOptionsSchema = ipsecExistingCertificateExportIntentSchema.extend({ caPrivateKeyExport: z.boolean(), requestKey: ipsecExistingCertificateExportRequestKeySchema, }).strict() export type IpsecExistingCertExportOptions = z.infer function existingCertExportIntent(passphrase: string, options: IpsecExistingCertExportOptions) { const parsed = existingCertExportOptionsSchema.safeParse(options) if (!parsed.success || !z.string().min(8).max(16384).safeParse(passphrase).success) { throw new Error("Экспорт требует явного подтверждения приватного ключа и UUID запроса") } return parsed.data } export async function exportIpsecUserCert( baseUrl: string, serverId: string, clientId: string, passphrase: string, options: IpsecExistingCertExportOptions, control?: { signal?: AbortSignal }, ): Promise { const intent = existingCertExportIntent(passphrase, options) const response = await requestJson( baseUrl, `/api/ipsec/users/${encodeURIComponent(serverId)}/${encodeURIComponent(clientId)}/cert`, { method: "POST", signal: control?.signal, headers: { "Idempotency-Key": intent.requestKey }, body: JSON.stringify({ passphrase, privateKeyExport: intent.privateKeyExport, caPrivateKeyExport: intent.caPrivateKeyExport }) }, ) return parseExportBundle(response) } export async function exportIpsecCertByName( baseUrl: string, serverId: string, name: string, passphrase: string, options: IpsecExistingCertExportOptions, control?: { signal?: AbortSignal }, ): Promise { const intent = existingCertExportIntent(passphrase, options) const response = await requestJson( baseUrl, `/api/ipsec/certs/${encodeURIComponent(serverId)}/export`, { method: "POST", signal: control?.signal, headers: { "Idempotency-Key": intent.requestKey }, body: JSON.stringify({ name, passphrase, privateKeyExport: intent.privateKeyExport, caPrivateKeyExport: intent.caPrivateKeyExport }) }, ) return parseExportBundle(response) } export async function restoreIpsecRevision( baseUrl: string, revisionId: string, serverId: string, ): Promise<{ ok: boolean }> { return requestJson<{ ok: boolean }>(baseUrl, `/api/ipsec/revisions/${encodeURIComponent(revisionId)}/restore`, { method: "POST", body: JSON.stringify({ serverId }), }) } export async function patchIpsecPeer( baseUrl: string, serverId: string, peerId: string, patch: IpsecPeerPatch, ): Promise<{ ok: boolean }> { const result = await requestJson<{ ok: boolean }>( baseUrl, `/api/ipsec/peers/${encodeURIComponent(serverId)}/${encodeURIComponent(peerId)}`, { method: "PATCH", body: JSON.stringify(patch) }, ) if (result?.ok !== true) throw new Error("Сервер не подтвердил сохранение peer") return result } export async function deleteIpsecPeer( baseUrl: string, serverId: string, peerId: string, opts?: { force?: boolean }, ): Promise<{ ok: boolean }> { const q = new URLSearchParams() if (opts?.force) q.set("force", "true") const suffix = q.size > 0 ? `?${q.toString()}` : "" const result = await requestJson<{ ok: boolean }>( baseUrl, `/api/ipsec/peers/${encodeURIComponent(serverId)}/${encodeURIComponent(peerId)}${suffix}`, { method: "DELETE" }, ) if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление") return result } export async function deleteIpsecCert( baseUrl: string, serverId: string, name: string, opts?: { force?: boolean }, ): Promise<{ ok: boolean }> { const q = new URLSearchParams() if (opts?.force) q.set("force", "true") const suffix = q.size > 0 ? `?${q.toString()}` : "" const result = await requestJson<{ ok: boolean }>( baseUrl, `/api/ipsec/certs/${encodeURIComponent(serverId)}${suffix}`, { method: "DELETE", body: JSON.stringify({ name }) }, ) if (result?.ok !== true) throw new Error("Сервер не подтвердил удаление") return result }