diff --git a/packages/libs/lib-server/src/system/settings/service/models.ts b/packages/libs/lib-server/src/system/settings/service/models.ts
index aae62daeb..bd13f2442 100644
--- a/packages/libs/lib-server/src/system/settings/service/models.ts
+++ b/packages/libs/lib-server/src/system/settings/service/models.ts
@@ -123,14 +123,16 @@ export class SysPrivateSettings extends BaseSettings {
config: {},
};
- // 证书颁发机构配置列表(内置 + 自定义ACME),内置项 builtIn=true 不允许修改删除
+ // 证书颁发机构配置列表(内置 + 自定义ACME),内置项 builtIn=true 不允许修改删除。
+ // 内置项不在此配置 Directory URL:其端点由运行时按加密算法(pkType)通过 acme.getDirectoryUrl 获取,
+ // 例如 ZeroSSL、SSL.com 的 RSA 与 EC 证书使用不同的端点,写死单一 URL 会导致 EC 申请走错端点。
customAcmeProviders?: CustomAcmeProvider[] = [
- { sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "https://acme-v02.api.letsencrypt.org/directory", needEAB: false, builtIn: true },
- { sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "https://acme-staging-v02.api.letsencrypt.org/directory", needEAB: false, builtIn: true },
- { sslProvider: "google", title: "Google", directoryUrl: "https://dv.acme-v02.api.pki.goog/directory", needEAB: true, builtIn: true },
- { sslProvider: "zerossl", title: "ZeroSSL", directoryUrl: "https://acme.zerossl.com/v2/DV90", needEAB: true, builtIn: true },
- { sslProvider: "litessl", title: "litessl", directoryUrl: "https://acme.litessl.com/acme/v2/directory", needEAB: true, builtIn: true },
- { sslProvider: "sslcom", title: "SSL.com", directoryUrl: "https://acme.ssl.com/sslcom-dv-rsa", needEAB: true, builtIn: true },
+ { sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "", needEAB: false, builtIn: true },
+ { sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "", needEAB: false, builtIn: true },
+ { sslProvider: "google", title: "Google", directoryUrl: "", needEAB: true, builtIn: true },
+ { sslProvider: "zerossl", title: "ZeroSSL", directoryUrl: "", needEAB: true, builtIn: true },
+ { sslProvider: "litessl", title: "litessl", directoryUrl: "", needEAB: true, builtIn: true },
+ { sslProvider: "sslcom", title: "SSL.com", directoryUrl: "", needEAB: true, builtIn: true },
];
removeSecret() {
diff --git a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue
index 78a5c7b35..e01adec6f 100644
--- a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue
+++ b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue
@@ -76,18 +76,30 @@ const props = defineProps<
>();
const emit = defineEmits<{
"update:value": any;
+ "selected-change": any;
}>();
function updateValue(value: any) {
if (props.single === true) {
const last = value?.[value.length - 1];
emit("update:value", last);
+ emitSelectedChange(last);
selectRef.value.blur();
} else {
emit("update:value", value);
+ emitSelectedChange(value);
}
+}
- // emit("update:value", value);
+/**
+ * 选中值变化时,通过 selected-change 事件把选中的 option 对象发射出去,
+ * 供字段 mergeScript 里 on: { "selected-change": (scope) => {...} } 联动使用
+ * (如根据选项的 needEAB 显隐其他字段)。单选发射单个 option,多选发射 option 数组。
+ */
+function emitSelectedChange(value: any) {
+ const values = Array.isArray(value) ? value : [value];
+ const options = values.map(v => optionsRef.value.find((item: any) => item.value === v)).filter(Boolean);
+ emit("selected-change", props.single === true ? options[0] : options);
}
const attrs = useAttrs();
diff --git a/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts b/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts
index 671b91ce7..a3ec6e538 100644
--- a/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts
+++ b/packages/ui/certd-client/src/locales/langs/en-US/certd/sys-settings.ts
@@ -50,7 +50,10 @@ export default {
customAcmeReverseProxy: "Reverse Proxy",
customAcmeReverseProxyHelper: "Optional; reverse proxy address when the ACME endpoint is not directly reachable, without http://",
customAcmeAdd: "Add Custom ACME",
+ customAcmeEdit: "Edit Custom ACME",
+ customAcmeNeedEab: "Need EAB",
customAcmeBuiltIn: "Built-in",
+ customAcmeTag: "Custom",
customAcmeDeleteConfirmTitle: "Delete Custom ACME",
customAcmeDeleteConfirmContent: "Deleting '{title}' may affect pipelines using it to apply certificates. Are you sure?",
customAcmeSaveCheck: "Name, sslProvider and Directory URL are required",
diff --git a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts
index 98fac929c..b123f426a 100644
--- a/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts
+++ b/packages/ui/certd-client/src/locales/langs/zh-CN/certd/sys-settings.ts
@@ -49,7 +49,10 @@ export default {
customAcmeReverseProxy: "反向代理地址",
customAcmeReverseProxyHelper: "选填;ACME端点无法直连时的反代地址,不带http://",
customAcmeAdd: "添加自定义ACME",
+ customAcmeEdit: "编辑自定义ACME",
+ customAcmeNeedEab: "需要EAB",
customAcmeBuiltIn: "内置",
+ customAcmeTag: "自定义",
customAcmeDeleteConfirmTitle: "删除自定义ACME",
customAcmeDeleteConfirmContent: "删除「{title}」可能影响使用它的流水线申请证书,确认是否删除?",
customAcmeSaveCheck: "自定义ACME的名称、标识、Directory URL均为必填",
diff --git a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx
index 7385203c3..42d8993b4 100644
--- a/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx
+++ b/packages/ui/certd-client/src/views/certd/pipeline/certd-form/use.tsx
@@ -1,6 +1,6 @@
import { checkPipelineLimit } from "/@/views/certd/pipeline/utils";
import { cloneDeep, merge, omit } from "lodash-es";
-import { notification, Modal } from "ant-design-vue";
+import { notification, Modal, message } from "ant-design-vue";
import { nanoid } from "nanoid";
import { useRouter } from "vue-router";
import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud";
@@ -237,7 +237,7 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
content: pickCertApplyTemplateParams(form.input),
});
await reloadApplyTemplates();
- notification.success({ message: "保存成功" });
+ // notification.success({ message: "保存成功" });
},
},
},
@@ -620,7 +620,8 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
console.error(e);
}
}
- notification.success({ message: "创建成功,请添加证书部署任务" });
+ //这里要用message 因为流水线详情页面,右上角有编辑保存按钮,Notification会把他们挡住
+ message.success("创建成功,请添加证书部署任务");
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
}
const certPlugins = await getCertPlugins();
diff --git a/packages/ui/certd-client/src/views/sys/settings/components/custom-acme-provider-list.vue b/packages/ui/certd-client/src/views/sys/settings/components/custom-acme-provider-list.vue
new file mode 100644
index 000000000..4491f7826
--- /dev/null
+++ b/packages/ui/certd-client/src/views/sys/settings/components/custom-acme-provider-list.vue
@@ -0,0 +1,171 @@
+
+
+
+
{{ t("certd.sys.setting.customAcmeBuiltIn") }}
+
{{ t("certd.sys.setting.customAcmeTag") }}
+
{{ provider.title }}
+
+
{{ t("common.edit") }}
+
{{ t("common.delete") }}
+
+
+
+ {{ t("certd.sys.setting.customAcmeAdd") }}
+
+
+
+
+
diff --git a/packages/ui/certd-client/src/views/sys/settings/tabs/pipeline.vue b/packages/ui/certd-client/src/views/sys/settings/tabs/pipeline.vue
index e4daa0f59..3919d16dc 100644
--- a/packages/ui/certd-client/src/views/sys/settings/tabs/pipeline.vue
+++ b/packages/ui/certd-client/src/views/sys/settings/tabs/pipeline.vue
@@ -60,29 +60,12 @@
{{ t("certd.sys.setting.acmeWalkFromAuthoritativeHelper") }}
-
-
-
-
{{ t("certd.sys.setting.customAcmeManage") }}
-
{{ t("certd.sys.setting.customAcmeManageHelper") }}
-
-
-
{{ t("certd.sys.setting.customAcmeBuiltIn") }}
-
-
-
-
-
-
-
-
-
+
+
+
{{ t("certd.sys.setting.customAcmeManageHelper") }}
+
-
-
-
+ {{ t("certd.sys.setting.customAcmeAdd") }}
-
-
+
{{ t("certd.saveButton") }}
@@ -92,13 +75,13 @@
-
+
diff --git a/packages/ui/certd-server/db/migration-mysql/v10052__plugin_market_columns.sql b/packages/ui/certd-server/db/migration-mysql/v10052__plugin_market_columns.sql
new file mode 100644
index 000000000..a58e8594e
--- /dev/null
+++ b/packages/ui/certd-server/db/migration-mysql/v10052__plugin_market_columns.sql
@@ -0,0 +1,16 @@
+ALTER TABLE `pi_plugin` ADD COLUMN `app_id` bigint;
+ALTER TABLE `pi_plugin` ADD COLUMN `developer_id` bigint;
+ALTER TABLE `pi_plugin` ADD COLUMN `full_name` varchar(191);
+ALTER TABLE `pi_plugin` ADD COLUMN `latest` varchar(100);
+ALTER TABLE `pi_plugin` ADD COLUMN `status` varchar(100);
+ALTER TABLE `pi_plugin` ADD COLUMN `download_count` bigint;
+ALTER TABLE `pi_plugin` ADD COLUMN `score` real;
+ALTER TABLE `pi_plugin` ADD COLUMN `sync_time` bigint;
+
+CREATE UNIQUE INDEX `index_plugin_full_name` ON `pi_plugin` (`full_name`);
+CREATE INDEX `index_plugin_plugin_type` ON `pi_plugin` (`pluginType`);
+CREATE INDEX `index_plugin_group` ON `pi_plugin` (`group`);
+CREATE INDEX `index_plugin_developer_id` ON `pi_plugin` (`developer_id`);
+CREATE INDEX `index_plugin_sync_time` ON `pi_plugin` (`sync_time`);
+
+UPDATE `pi_plugin` SET `type` = 'store' WHERE `type` = 'custom';
diff --git a/packages/ui/certd-server/db/migration-mysql/v10053__plugin_installed.sql b/packages/ui/certd-server/db/migration-mysql/v10053__plugin_installed.sql
new file mode 100644
index 000000000..2174d8fec
--- /dev/null
+++ b/packages/ui/certd-server/db/migration-mysql/v10053__plugin_installed.sql
@@ -0,0 +1,22 @@
+ALTER TABLE `pi_plugin` ADD COLUMN `installed` boolean NOT NULL DEFAULT true;
+
+UPDATE `pi_plugin` SET `installed` = true;
+
+ALTER TABLE `pi_plugin` ADD COLUMN `ai_check_status` varchar(32) NOT NULL DEFAULT '';
+
+ALTER TABLE `pi_plugin` ADD COLUMN `vip` varchar(50);
+
+-- 旧版插件没有 full_name,以作者和插件名回填,后续统一以 full_name 识别插件。
+-- MySQL 中 || 默认是逻辑或,字符串拼接必须用 CONCAT。
+UPDATE `pi_plugin`
+SET `full_name` = CASE
+ WHEN `author` IS NOT NULL AND TRIM(`author`) <> '' THEN CONCAT(TRIM(`author`), '/', `name`)
+ ELSE `name`
+END
+WHERE `full_name` IS NULL OR TRIM(`full_name`) = '';
+
+ALTER TABLE `cd_domain` ADD COLUMN `remark` varchar(500) NOT NULL DEFAULT '';
+
+-- 在线插件依赖声明使用独立字段存储,避免塞进 extra YAML 解析合并。
+-- 旧数据里 extra 中的 dependPlugins 由代码读取时兜底兼容,下次同步后自动迁移到新字段。
+ALTER TABLE `pi_plugin` ADD COLUMN `depend_plugins` varchar(1024);
diff --git a/packages/ui/certd-server/db/migration-mysql/v10054__cert_info_status.sql b/packages/ui/certd-server/db/migration-mysql/v10054__cert_info_status.sql
new file mode 100644
index 000000000..76c61ce94
--- /dev/null
+++ b/packages/ui/certd-server/db/migration-mysql/v10054__cert_info_status.sql
@@ -0,0 +1,11 @@
+
+-- 证书仓库记录增加状态字段:active=激活,inactive=未激活,revoked=已吊销
+ALTER TABLE cd_cert_info ADD COLUMN status varchar(20) NOT NULL DEFAULT 'active';
+-- 吊销时间
+ALTER TABLE cd_cert_info ADD COLUMN revoke_time bigint NULL;
+-- 证书申请任务id:用于吊销旧证书时按任务id精确匹配(只吊销同一申请任务产出的旧证书)
+ALTER TABLE cd_cert_info ADD COLUMN task_id varchar(100) NULL;
+
+
+-- 证书仓库记录增加ACME账号授权id:吊销旧证书时直接使用记录上的账号,无需再解析流水线配置
+ALTER TABLE cd_cert_info ADD COLUMN acme_account_access_id bigint NULL;
diff --git a/packages/ui/certd-server/db/migration-pg/v10052__plugin_market_columns.sql b/packages/ui/certd-server/db/migration-pg/v10052__plugin_market_columns.sql
new file mode 100644
index 000000000..f9700aaf7
--- /dev/null
+++ b/packages/ui/certd-server/db/migration-pg/v10052__plugin_market_columns.sql
@@ -0,0 +1,16 @@
+ALTER TABLE "pi_plugin" ADD COLUMN "app_id" bigint;
+ALTER TABLE "pi_plugin" ADD COLUMN "developer_id" bigint;
+ALTER TABLE "pi_plugin" ADD COLUMN "full_name" varchar(191);
+ALTER TABLE "pi_plugin" ADD COLUMN "latest" varchar(100);
+ALTER TABLE "pi_plugin" ADD COLUMN "status" varchar(100);
+ALTER TABLE "pi_plugin" ADD COLUMN "download_count" bigint;
+ALTER TABLE "pi_plugin" ADD COLUMN "score" real;
+ALTER TABLE "pi_plugin" ADD COLUMN "sync_time" bigint;
+
+CREATE UNIQUE INDEX "index_plugin_full_name" ON "pi_plugin" ("full_name");
+CREATE INDEX "index_plugin_plugin_type" ON "pi_plugin" ("pluginType");
+CREATE INDEX "index_plugin_group" ON "pi_plugin" ("group");
+CREATE INDEX "index_plugin_developer_id" ON "pi_plugin" ("developer_id");
+CREATE INDEX "index_plugin_sync_time" ON "pi_plugin" ("sync_time");
+
+UPDATE "pi_plugin" SET "type" = 'store' WHERE "type" = 'custom';
diff --git a/packages/ui/certd-server/db/migration-pg/v10053__plugin_installed.sql b/packages/ui/certd-server/db/migration-pg/v10053__plugin_installed.sql
new file mode 100644
index 000000000..1b51cc2ad
--- /dev/null
+++ b/packages/ui/certd-server/db/migration-pg/v10053__plugin_installed.sql
@@ -0,0 +1,21 @@
+ALTER TABLE "pi_plugin" ADD COLUMN "installed" boolean NOT NULL DEFAULT true;
+
+UPDATE "pi_plugin" SET "installed" = true;
+
+ALTER TABLE "pi_plugin" ADD COLUMN "ai_check_status" varchar(32) NOT NULL DEFAULT '';
+
+ALTER TABLE "pi_plugin" ADD COLUMN "vip" varchar(50);
+
+-- 旧版插件没有 full_name,以作者和插件名回填,后续统一以 full_name 识别插件。
+UPDATE "pi_plugin"
+SET "full_name" = CASE
+ WHEN "author" IS NOT NULL AND TRIM("author") <> '' THEN TRIM("author") || '/' || "name"
+ ELSE "name"
+END
+WHERE "full_name" IS NULL OR TRIM("full_name") = '';
+
+ALTER TABLE "cd_domain" ADD COLUMN "remark" varchar(500) NOT NULL DEFAULT '';
+
+-- 在线插件依赖声明使用独立字段存储,避免塞进 extra YAML 解析合并。
+-- 旧数据里 extra 中的 dependPlugins 由代码读取时兜底兼容,下次同步后自动迁移到新字段。
+ALTER TABLE "pi_plugin" ADD COLUMN "depend_plugins" varchar(1024);
diff --git a/packages/ui/certd-server/db/migration-pg/v10054__cert_info_status.sql b/packages/ui/certd-server/db/migration-pg/v10054__cert_info_status.sql
new file mode 100644
index 000000000..76c61ce94
--- /dev/null
+++ b/packages/ui/certd-server/db/migration-pg/v10054__cert_info_status.sql
@@ -0,0 +1,11 @@
+
+-- 证书仓库记录增加状态字段:active=激活,inactive=未激活,revoked=已吊销
+ALTER TABLE cd_cert_info ADD COLUMN status varchar(20) NOT NULL DEFAULT 'active';
+-- 吊销时间
+ALTER TABLE cd_cert_info ADD COLUMN revoke_time bigint NULL;
+-- 证书申请任务id:用于吊销旧证书时按任务id精确匹配(只吊销同一申请任务产出的旧证书)
+ALTER TABLE cd_cert_info ADD COLUMN task_id varchar(100) NULL;
+
+
+-- 证书仓库记录增加ACME账号授权id:吊销旧证书时直接使用记录上的账号,无需再解析流水线配置
+ALTER TABLE cd_cert_info ADD COLUMN acme_account_access_id bigint NULL;
diff --git a/packages/ui/certd-server/db/migration/v10052__plugin_market_columns.sql b/packages/ui/certd-server/db/migration/v10052__plugin_market_columns.sql
index db7073f44..6d2543b5b 100644
--- a/packages/ui/certd-server/db/migration/v10052__plugin_market_columns.sql
+++ b/packages/ui/certd-server/db/migration/v10052__plugin_market_columns.sql
@@ -1,6 +1,6 @@
ALTER TABLE "pi_plugin" ADD COLUMN "app_id" integer;
ALTER TABLE "pi_plugin" ADD COLUMN "developer_id" integer;
-ALTER TABLE "pi_plugin" ADD COLUMN "full_name" varchar(200);
+ALTER TABLE "pi_plugin" ADD COLUMN "full_name" varchar(191);
ALTER TABLE "pi_plugin" ADD COLUMN "latest" varchar(100);
ALTER TABLE "pi_plugin" ADD COLUMN "status" varchar(100);
ALTER TABLE "pi_plugin" ADD COLUMN "download_count" integer;
diff --git a/packages/ui/certd-server/db/migration/v10053__plugin_installed.sql b/packages/ui/certd-server/db/migration/v10053__plugin_installed.sql
index c7f737e00..1b51cc2ad 100644
--- a/packages/ui/certd-server/db/migration/v10053__plugin_installed.sql
+++ b/packages/ui/certd-server/db/migration/v10053__plugin_installed.sql
@@ -15,3 +15,7 @@ END
WHERE "full_name" IS NULL OR TRIM("full_name") = '';
ALTER TABLE "cd_domain" ADD COLUMN "remark" varchar(500) NOT NULL DEFAULT '';
+
+-- 在线插件依赖声明使用独立字段存储,避免塞进 extra YAML 解析合并。
+-- 旧数据里 extra 中的 dependPlugins 由代码读取时兜底兼容,下次同步后自动迁移到新字段。
+ALTER TABLE "pi_plugin" ADD COLUMN "depend_plugins" varchar(1024);
diff --git a/packages/ui/certd-server/db/migration/v10055__plugin_depend_plugins.sql b/packages/ui/certd-server/db/migration/v10055__plugin_depend_plugins.sql
deleted file mode 100644
index 4127fa88e..000000000
--- a/packages/ui/certd-server/db/migration/v10055__plugin_depend_plugins.sql
+++ /dev/null
@@ -1,3 +0,0 @@
--- 在线插件依赖声明使用独立字段存储,避免塞进 extra YAML 解析合并。
--- 旧数据里 extra 中的 dependPlugins 由代码读取时兜底兼容,下次同步后自动迁移到新字段。
-ALTER TABLE "pi_plugin" ADD COLUMN "depend_plugins" varchar(4096);
diff --git a/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts b/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts
index 06d36cffb..31f5892bc 100644
--- a/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts
+++ b/packages/ui/certd-server/src/controller/sys/settings/sys-settings-controller.ts
@@ -1,4 +1,4 @@
-import { getRuntimeDepsService } from "@certd/pipeline";
+import { getRuntimeDepsService } from "@certd/pipeline";
import { ALL, Body, Controller, Inject, Post, Provide, Query, RequestIP } from "@midwayjs/core";
import { addonRegistry, AddonService, CrudController, SysPrivateSettings, SysPublicSettings, SysSafeSetting, SysSettingsEntity, SysSettingsService } from "@certd/lib-server";
import { cloneDeep, merge } from "lodash-es";
@@ -136,6 +136,10 @@ export class SysSettingsController extends CrudController {
const privateSettings = await this.service.getPrivateSettings();
merge(publicSettings, body.public);
merge(privateSettings, body.private);
+ // customAcmeProviders 是数组,lodash merge 按下标合并,删除/调整顺序不会生效,需整体替换
+ if (body.private?.customAcmeProviders !== undefined) {
+ privateSettings.customAcmeProviders = body.private.customAcmeProviders;
+ }
await this.service.savePublicSettings(publicSettings);
await this.service.savePrivateSettings(privateSettings);
await this.auditLog({ content: "保存了系统设置" });
diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.test.ts b/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.test.ts
index d86c1c68b..33b20e278 100644
--- a/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.test.ts
+++ b/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.test.ts
@@ -59,6 +59,13 @@ describe("AcmeAccountAccess", () => {
});
describe("AcmeAccountAccess custom CA", () => {
+ it("颁发机构为空时 getDirectoryUrl 给出明确提示", async () => {
+ const access = new AcmeAccountAccess();
+ access.caType = null as any;
+
+ await assert.rejects(() => access.getDirectoryUrl(), /请先选择颁发机构/);
+ });
+
it("custom caType 返回自定义 directoryUrl(旧版兼容)", async () => {
const access = new AcmeAccountAccess();
access.caType = "custom";
diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts b/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts
index e8e8bb30b..8a74553e6 100644
--- a/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts
+++ b/packages/ui/certd-server/src/plugins/plugin-cert/access/acme-account-access.ts
@@ -45,12 +45,18 @@ export class AcmeAccountAccess extends BaseAccess {
single: true,
},
required: true,
- helper: "自定义ACME:适用于内网CA、企业CA等,由管理员在「系统设置-流水线设置」中配置",
mergeScript: `
return {
component: {
- disabled: ctx.compute(({form})=> !!form.access?.account)
- }
+ disabled: ctx.compute(({form})=> !!form.access?.account),
+ on: {
+ "selected-change": (scope)=>{
+ // 把选中颁发机构的 needEAB 同步到表单隐藏字段,用于控制 EAB 输入框显隐
+ let form = scope.form || {};
+ form.access._needEAB = scope.$event?.needEAB
+ }
+ }
+ },
}
`,
})
@@ -104,18 +110,8 @@ export class AcmeAccountAccess extends BaseAccess {
mergeScript: `
return {
show: ctx.compute(({form})=>{
- const caType = form.access?.caType;
- // 内置颁发机构按 needEAB 配置显隐;自定义ACME显示EAB(选填)
- const builtInNeedEab = { letsencrypt: false, letsencrypt_staging: false, google: true, zerossl: true, sslcom: true, litessl: true };
- if (caType && builtInNeedEab[caType] !== undefined) {
- return builtInNeedEab[caType];
- }
- return !!caType;
- }),
- required: ctx.compute(({form})=>{
- const caType = form.access?.caType;
- // 内置需要EAB的颁发机构必填,自定义ACME选填
- return ['google','zerossl','sslcom','litessl'].includes(caType);
+ // 按所选颁发机构的 needEAB 配置显隐(由 caType 的 onSelectedChange 同步到 _needEAB)
+ return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
@@ -135,18 +131,8 @@ export class AcmeAccountAccess extends BaseAccess {
mergeScript: `
return {
show: ctx.compute(({form})=>{
- const caType = form.access?.caType;
- // 内置颁发机构按 needEAB 配置显隐;自定义ACME显示EAB(选填)
- const builtInNeedEab = { letsencrypt: false, letsencrypt_staging: false, google: true, zerossl: true, sslcom: true, litessl: true };
- if (caType && builtInNeedEab[caType] !== undefined) {
- return builtInNeedEab[caType];
- }
- return !!caType;
- }),
- required: ctx.compute(({form})=>{
- const caType = form.access?.caType;
- // 内置需要EAB的颁发机构必填,自定义ACME选填
- return ['google','zerossl','sslcom','litessl'].includes(caType);
+ // 按所选颁发机构的 needEAB 配置显隐(由 caType 的 onSelectedChange 同步到 _needEAB)
+ return form.access?._needEAB === true;
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
@@ -210,6 +196,9 @@ export class AcmeAccountAccess extends BaseAccess {
* - 自定义ACME:按 sslProvider 从「流水线设置」读取 directoryUrl
*/
async getDirectoryUrl() {
+ if (!this.caType) {
+ throw new Error("请先选择颁发机构");
+ }
if (this.caType === "custom") {
if (!this.directoryUrl) {
throw new Error("自定义ACME需要填写Directory URL");
@@ -287,6 +276,8 @@ export class AcmeAccountAccess extends BaseAccess {
sslProvider: this.caType as any,
// 自定义ACME时把 Directory URL 传给 AcmeService(生成账号必须走自定义端点);内置颁发机构走内置端点
directoryUrl: provider?.builtIn ? undefined : directoryUrl,
+ // 自定义ACME配置的反向代理地址,生成账号(访问directory/newAccount)时同样使用
+ reverseProxy: provider?.reverseProxy,
eab: externalAccountBinding ? ({ ...externalAccountBinding, id: 0 } as any) : undefined,
privateKeyType: "rsa_2048",
signal: (this.ctx as any)?.signal,
diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts
index 490348222..a9eac5ca1 100644
--- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts
+++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.test.ts
@@ -199,6 +199,36 @@ describe("AcmeService challenge", () => {
utils.http.request = originalRequest;
}
});
+
+ it("显式配置 reverseProxy 时强制启用 urlMapping,不再依赖直连测试", async () => {
+ const originalRequest = utils.http.request;
+ let requestCount = 0;
+ utils.http.request = async () => {
+ requestCount++;
+ return {};
+ };
+
+ try {
+ const service = new AcmeService({
+ userId: 1,
+ userContext: {} as any,
+ logger: logger as any,
+ sslProvider: "custom",
+ directoryUrl: "https://myca.example.com/directory",
+ reverseProxy: "myca-proxy.example.com",
+ domainParser: {} as any,
+ });
+
+ const urlMapping = await service.resolveUrlMapping("https://myca.example.com/directory");
+
+ // 配置了反向代理地址时直接启用,不进行直连测试
+ assert.equal(urlMapping.enabled, true);
+ assert.equal(urlMapping.mappings["myca.example.com"], "myca-proxy.example.com");
+ assert.equal(requestCount, 0);
+ } finally {
+ utils.http.request = originalRequest;
+ }
+ });
});
describe("AcmeService custom directoryUrl", () => {
diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts
index 3ed2bb14f..b239c8d60 100644
--- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts
+++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/acme.ts
@@ -201,6 +201,11 @@ export class AcmeService {
async resolveUrlMapping(directoryUrl: string) {
const urlMapping = this.buildUrlMapping(directoryUrl);
+ // 显式配置了反向代理地址(如自定义ACME配置的 reverseProxy)时,直接启用 urlMapping,保证反代始终生效
+ if (this.options.reverseProxy) {
+ urlMapping.enabled = true;
+ return urlMapping;
+ }
if (this.options.useMappingProxy) {
urlMapping.enabled = true;
return urlMapping;
diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.test.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.test.ts
index d93e3d438..678ae02d6 100644
--- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.test.ts
+++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.test.ts
@@ -224,7 +224,7 @@ describe("CertApplyPlugin custom 颁发机构", () => {
return {
sslProvider: "letsencrypt",
title: "Let's Encrypt",
- directoryUrl: "https://acme-v02.api.letsencrypt.org/directory",
+ directoryUrl: "",
builtIn: true,
};
},
@@ -263,9 +263,9 @@ describe("CertApplyPlugin custom 颁发机构", () => {
serviceGetter: createCustomAcmeServiceGetter({
async getAll() {
return [
- { sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "https://acme-v02.api.letsencrypt.org/directory", builtIn: true },
- { sslProvider: "google", title: "Google", directoryUrl: "https://dv.acme-v02.api.pki.goog/directory", builtIn: true },
- { sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "https://acme-staging-v02.api.letsencrypt.org/directory", builtIn: true },
+ { sslProvider: "letsencrypt", title: "Let's Encrypt", directoryUrl: "", builtIn: true },
+ { sslProvider: "google", title: "Google", directoryUrl: "", builtIn: true },
+ { sslProvider: "letsencrypt_staging", title: "Let's Encrypt测试环境", directoryUrl: "", builtIn: true },
{ sslProvider: "myca", title: "我的CA", directoryUrl: "https://myca.example.com/directory", builtIn: false },
{ sslProvider: "myca2", title: "二号CA", directoryUrl: "https://myca2.example.com/directory", builtIn: false },
];
@@ -285,6 +285,20 @@ describe("CertApplyPlugin custom 颁发机构", () => {
assert.equal(values.includes("custom"), false);
});
+ it("颁发机构为空时 onInit 给出明确提示", async () => {
+ const plugin: any = new CertApplyPlugin();
+ plugin.version = 2;
+ plugin.sslProvider = null;
+ plugin.logger = { info() {}, warn() {}, error() {}, debug() {} };
+ plugin.ctx = {
+ serviceGetter: createCustomAcmeServiceGetter({}),
+ user: { id: 1 },
+ signal: undefined,
+ };
+
+ await assert.rejects(() => plugin.onInit(), /请先选择证书颁发机构/);
+ });
+
it("自定义颁发机构不支持 DNS 持久验证", async () => {
const plugin: any = new CertApplyPlugin();
plugin.version = 2;
diff --git a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts
index 20ef92a4b..584582d9c 100644
--- a/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts
+++ b/packages/ui/certd-server/src/plugins/plugin-cert/plugin/cert-plugin/apply.ts
@@ -257,15 +257,17 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
single: true,
value: "letsencrypt",
required: true,
- helper:
- "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,仅首次需要翻墙获取EAB授权,无需翻墙\nSSL.com:仅主域名和www免费,必须设置CAA记录\n自定义ACME:适用于内网CA、企业CA等,由管理员在「系统设置-流水线设置」中配置",
+ helper: "Let's Encrypt:申请最简单\nGoogle:大厂光环,兼容性好,仅首次需要翻墙获取EAB授权,无需翻墙\nSSL.com:仅主域名和www免费,必须设置CAA记录\n自定义ACME:管理员可在「系统设置-流水线设置」中配置",
mergeScript: `return {
component:{
- onSelectedChange: ctx.compute(({form})=>{
- return ($event)=>{
+ on: {
+ "selected-change": (scope)=>{
+ // 切换颁发机构后,清空已选择的ACME账号,避免账号与颁发机构不匹配
+ let form = scope.form || {};
+ form = form.input || form.body || form;
form.acmeAccountAccessId = null
}
- })
+ }
}
}
`,
@@ -569,11 +571,17 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
private acmeProvider?: CustomAcmeProvider;
async onInit() {
- let eab: EabAccess = null;
+
- // 解析当前颁发机构的系统配置(内置 + 自定义),自定义未配置时直接报错
- this.acmeProvider = await this.getAcmeProvider();
+
+ }
+ private async getAcmeClient() {
+ if (this.acme) {
+ return this.acme;
+ }
+
+ let eab: EabAccess = null;
const isNewVersion = this.version === 2;
// 内置非 LE 颁发机构需要走EAB获取流程;自定义ACME无需EAB(其EAB在ACME账号授权中按需选填)
if (!isNewVersion && this.sslProvider && !this.sslProvider.startsWith("letsencrypt") && this.acmeProvider?.builtIn) {
@@ -603,6 +611,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
this.eab = eab;
const subDomainsGetter = await this.ctx.serviceGetter.get("subDomainsGetter");
const domainParser = new DomainParser(subDomainsGetter, this.logger);
+ // 解析当前颁发机构的系统配置(内置 + 自定义),自定义未配置时直接报错
+ this.acmeProvider = await this.getAcmeProvider();
this.acme = new AcmeService({
userId: this.ctx.user.id,
@@ -622,12 +632,16 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
domainParser,
waitDnsDiffuseTime: this.waitDnsDiffuseTime,
});
+ return this.acme;
}
/**
* 获取当前颁发机构在系统「流水线设置」中的配置(内置 + 自定义);未找到时给出明确报错
*/
private async getAcmeProvider(): Promise {
+ if (!this.sslProvider) {
+ throw new Error("请先选择证书颁发机构");
+ }
const customAcmeProviderService: any = await this.ctx.serviceGetter.get("customAcmeProviderService");
const provider = await customAcmeProviderService.getBySslProvider(this.sslProvider);
if (!provider) {
@@ -651,6 +665,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}
async doCertApply() {
+ await this.getAcmeClient();
// 自定义ACME不支持DNS持久验证(_validation-persist 为 Let's Encrypt 特有机制)
if (this.acmeProvider && this.acmeProvider.builtIn !== true && this.challengeType === "dns-persist") {
throw new Error("自定义ACME不支持DNS持久验证,请改用其他域名验证方式");