feat: 支持证书托管和节点自动安装

This commit is contained in:
2026-09-29 13:06:56 +08:00
parent ab7d1c937a
commit 586c2399d1
20 changed files with 1600 additions and 20 deletions
+67
View File
@@ -0,0 +1,67 @@
"use server";
import { revalidatePath } from "next/cache";
import { requireAdminAccess } from "@/lib/session";
import {
saveCertificate,
deleteCertificate,
ProvisionError,
} from "@/lib/certificates";
import {
createInstallation,
installationSchema,
revokeInstallation,
} from "@/lib/provisioning";
async function run(action: () => Promise<unknown>) {
await requireAdminAccess();
try {
await action();
} catch (error) {
if (error instanceof ProvisionError) return { error: error.message };
throw error;
}
revalidatePath("/admin/certificates");
revalidatePath("/admin/nodes");
}
export async function uploadCertificateAction(data: FormData) {
return run(async () => {
const crt = data.get("certificate"),
key = data.get("private_key");
if (
!(crt instanceof File) ||
!(key instanceof File) ||
crt.size > 128 * 1024 ||
key.size > 32 * 1024
)
throw new ProvisionError(
"请选择 PEM 格式的 CRT(最多 128 KB)和 KEY(最多 32 KB)文件。",
);
await saveCertificate(
String(data.get("name") ?? ""),
await crt.text(),
await key.text(),
);
});
}
export async function deleteCertificateAction(data: FormData) {
return run(() => deleteCertificate(Number(data.get("id"))));
}
export async function createInstallationAction(data: FormData) {
return run(async () => {
const input = installationSchema.safeParse({
name: data.get("name"),
slug: data.get("slug"),
certificateId: data.get("certificate_id"),
});
if (!input.success)
throw new ProvisionError(
"请检查输入:Slug 使用小写字母、数字或连字符,并选择覆盖 *.p.xn--876a.net 的证书。",
);
await createInstallation(input.data);
});
}
export async function revokeInstallationAction(data: FormData) {
return run(() =>
revokeInstallation(String(data.get("id")), data.get("remove") === "yes"),
);
}