feat: 支持证书托管和节点自动安装
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { X509Certificate, createPrivateKey } from "node:crypto";
|
||||
import { ensureDb } from "@/lib/db";
|
||||
import { encryptText } from "@/lib/crypto";
|
||||
|
||||
export class ProvisionError extends Error {}
|
||||
export type ManagedCertificate = {
|
||||
id: number;
|
||||
name: string;
|
||||
certificate_pem: string;
|
||||
private_key_encrypted: string;
|
||||
expires_at: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export function validateCertificate(pem: string, key: string, sni?: string) {
|
||||
if (pem.length > 128 * 1024 || key.length > 32 * 1024)
|
||||
throw new ProvisionError("证书或私钥文件过大。");
|
||||
let certificate: X509Certificate;
|
||||
try {
|
||||
certificate = new X509Certificate(pem);
|
||||
const privateKey = createPrivateKey(key);
|
||||
if (!certificate.checkPrivateKey(privateKey)) throw new Error();
|
||||
const chain = pem.match(
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g,
|
||||
);
|
||||
if (!chain?.length) throw new Error();
|
||||
chain.forEach((part) => new X509Certificate(part));
|
||||
} catch {
|
||||
throw new ProvisionError(
|
||||
"无法解析 PEM 证书/私钥,或私钥与证书不匹配。请上传未加密的私钥和完整证书链。",
|
||||
);
|
||||
}
|
||||
if (
|
||||
Date.parse(certificate.validFrom) > Date.now() ||
|
||||
Date.parse(certificate.validTo) <= Date.now()
|
||||
)
|
||||
throw new ProvisionError("证书尚未生效或已过期。");
|
||||
if (sni && !certificate.checkHost(sni, { subject: "never" }))
|
||||
throw new ProvisionError("SNI 域名与证书的 SAN 不匹配。");
|
||||
return certificate;
|
||||
}
|
||||
|
||||
export async function saveCertificate(name: string, pem: string, key: string) {
|
||||
if (!name.trim() || name.length > 100)
|
||||
throw new ProvisionError("请填写 1–100 字的证书名称。");
|
||||
const certificate = validateCertificate(pem, key);
|
||||
const db = await ensureDb();
|
||||
return db
|
||||
.prepare(
|
||||
`INSERT INTO managed_certificates
|
||||
(name,certificate_pem,private_key_encrypted,expires_at,created_at) VALUES (?,?,?,?,?)`,
|
||||
)
|
||||
.run(
|
||||
name.trim(),
|
||||
pem,
|
||||
encryptText(key),
|
||||
new Date(certificate.validTo).toISOString(),
|
||||
new Date().toISOString(),
|
||||
).lastInsertRowid;
|
||||
}
|
||||
|
||||
export async function listCertificates() {
|
||||
const db = await ensureDb();
|
||||
return db
|
||||
.prepare(
|
||||
"SELECT id,name,expires_at,created_at FROM managed_certificates ORDER BY id DESC",
|
||||
)
|
||||
.all() as Pick<
|
||||
ManagedCertificate,
|
||||
"id" | "name" | "expires_at" | "created_at"
|
||||
>[];
|
||||
}
|
||||
|
||||
export async function deleteCertificate(id: number) {
|
||||
const db = await ensureDb();
|
||||
if (
|
||||
db
|
||||
.prepare("SELECT 1 FROM node_installations WHERE certificate_id=?")
|
||||
.get(id)
|
||||
)
|
||||
throw new ProvisionError(
|
||||
"此证书仍关联安装记录,请先删除相关安装记录。已安装服务器的证书文件不会随之删除。",
|
||||
);
|
||||
db.prepare("DELETE FROM managed_certificates WHERE id=?").run(id);
|
||||
}
|
||||
Reference in New Issue
Block a user