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 @@ + + + + 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持久验证,请改用其他域名验证方式");