249 lines
5.9 KiB
TypeScript
249 lines
5.9 KiB
TypeScript
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<string, string> {
|
|
const headers: Record<string, string> = {
|
|
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<T>(
|
|
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<PleskSubscription[]>(
|
|
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<PleskCliResult>(
|
|
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<PleskCliResult>(
|
|
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<PleskDomain[]>(
|
|
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,
|
|
]);
|
|
}
|