import { decryptPleskSecret } from "@/lib/crypto"; export type PleskAuthType = "api_key" | "basic"; export type PleskConnection = { baseUrl: string; authType: PleskAuthType; encryptedSecret: string; }; export type PleskSubscription = { id?: string | number; guid?: string; subscription_id?: string | number; external_id?: string; name?: string; status?: string; owner_login?: string; owner?: { login?: string }; plan_name?: string; service_plan?: { name?: string }; }; export type PleskDomain = { id?: string | number; guid?: string; name?: string; domain_name?: string; domain?: string; status?: string; subscription?: { id?: string | number }; subscription_id?: string | number; webspace_id?: string | number; }; type PleskCliResult = { code?: number; stdout?: string; stderr?: string; }; const DEFAULT_TIMEOUT_MS = 12_000; function normalizeBaseUrl(baseUrl: string) { return baseUrl.trim().replace(/\/+$/, ""); } function buildHeaders( authType: PleskAuthType, plainSecret: string, ): Record { const headers: Record = { Accept: "application/json", "Content-Type": "application/json", }; if (authType === "api_key") { headers["X-API-Key"] = plainSecret; } else { headers.Authorization = `Basic ${Buffer.from(plainSecret).toString("base64")}`; } return headers; } async function withTimeout( input: RequestInfo | URL, init: RequestInit, timeoutMs = DEFAULT_TIMEOUT_MS, ) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { return await fetch(input, { ...init, signal: controller.signal, cache: "no-store", }); } finally { clearTimeout(timeout); } } export async function pleskRequest( connection: PleskConnection, path: string, init: RequestInit = {}, retries = 1, ) { const base = normalizeBaseUrl(connection.baseUrl); const url = `${base}${path.startsWith("/") ? path : `/${path}`}`; const secret = decryptPleskSecret(connection.encryptedSecret); let lastError: Error | null = null; for (let attempt = 0; attempt <= retries; attempt += 1) { try { const res = await withTimeout(url, { ...init, headers: { ...buildHeaders(connection.authType, secret), ...(init.headers ?? {}), }, }); if (!res.ok) { const text = await res.text(); throw new Error( `Plesk request failed (${res.status}): ${text || "unknown error"}`, ); } const contentType = res.headers.get("content-type") ?? ""; if (!contentType.includes("application/json")) { return null as T; } return (await res.json()) as T; } catch (error) { lastError = error instanceof Error ? error : new Error("Plesk request failed"); if (attempt === retries) break; await new Promise((resolve) => setTimeout(resolve, 250 * (attempt + 1))); } } throw lastError ?? new Error("Plesk request failed"); } export async function testPleskConnection(connection: PleskConnection) { try { await pleskRequest(connection, "/api/v2/server"); return { ok: true as const }; } catch { await pleskRequest(connection, "/api/v2/subscriptions?limit=1"); return { ok: true as const }; } } export async function listPleskSubscriptions(connection: PleskConnection) { try { const all: PleskSubscription[] = []; for (let page = 1; page <= 20; page += 1) { const payload = await pleskRequest( connection, `/api/v2/subscriptions?page=${page}&per_page=100`, ); const rows = Array.isArray(payload) ? payload : []; all.push(...rows); if (rows.length < 100) break; } return all; } catch { const cli = await pleskRequest( connection, "/api/v2/cli/subscription/call", { method: "POST", body: JSON.stringify({ params: ["--list"] }), }, ); const stdout = cli?.stdout ?? ""; if (!stdout.trim()) { return []; } return stdout .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean) .map((line) => { const columns = line.split(/\s+/); const externalId = columns[0] ?? line; return { id: externalId, subscription_id: externalId, external_id: externalId, name: columns.slice(1).join(" ") || externalId, status: null, }; }); } } export async function pleskCliCall( connection: PleskConnection, command: string, params: string[], ) { const payload = await pleskRequest( connection, `/api/v2/cli/${command}/call`, { method: "POST", body: JSON.stringify({ params }), }, ); const code = payload?.code ?? -1; const stdout = payload?.stdout ?? ""; const stderr = payload?.stderr ?? ""; if (code !== 0) { throw new Error( `Plesk CLI '${command}' failed (code ${code}): ${stderr || "unknown error"}${stdout ? `\n${stdout}` : ""}`, ); } return { code, stdout, stderr }; } export async function listPleskDomains(connection: PleskConnection) { const all: PleskDomain[] = []; for (let page = 1; page <= 20; page += 1) { const payload = await pleskRequest( connection, `/api/v2/domains?page=${page}&per_page=100`, ); const rows = Array.isArray(payload) ? payload : []; all.push(...rows); if (rows.length < 100) break; } return all; } export async function suspendPleskSubscription( connection: PleskConnection, subscriptionName: string, ) { return pleskCliCall(connection, "subscription", [ "--webspace-off", subscriptionName, ]); } export async function unsuspendPleskSubscription( connection: PleskConnection, subscriptionName: string, ) { return pleskCliCall(connection, "subscription", [ "--webspace-on", subscriptionName, ]); }