feat: 优化开放接口证书自动申请功能

1. 新增TextedException统一异常类
2. 新增AccessService获取默认授权方法
3. 扩展证书流水线批量更新字段,支持自动挑战类型、SSL提供商和ACME账户
4. 调整证书申请默认重试次数为1并优化重试逻辑
5. 优化证书模板获取逻辑,支持ID为0时获取默认模板
6. 完善自动流水线创建逻辑,默认设置挑战类型为auto
7. 调整默认证书链为ISRG Root X2
8. 优化自动申请证书的参数处理,补充邮箱、ACME账户和重试次数默认值
9. 替换部分异常抛出为TextedException
This commit is contained in:
xiaojunnuo
2026-08-09 22:38:29 +08:00
parent 1042e599d9
commit 6339cdffa9
9 changed files with 118 additions and 23 deletions
@@ -21,3 +21,10 @@ export class TextException extends BaseException {
super(name, code, message, data);
}
}
export class TextedException extends TextException {
constructor(res: { code: number; message: string; data?: any; name?: string }) {
const { code, message, data, name } = res;
super(name || "TextedException", code, message, data);
}
}
@@ -289,4 +289,15 @@ export class AccessService extends BaseService<AccessEntity> {
await this.repository.save(newAccess);
return newAccess.id;
}
async getDefaultByType({ type, userId, projectId, subtype }) {
return await this.repository.findOne({
where: {
type,
userId,
projectId,
subtype,
},
});
}
}
@@ -18,7 +18,7 @@ const emit = defineEmits<{
change: any;
}>();
const batchUpdateFields = ["renewDays", "privateKeyType"];
const batchUpdateFields = ["challengeType", "sslProvider", "acmeAccountAccessId", "renewDays", "privateKeyType"];
function hasFormValue(form: any, field: string) {
return form[field] != null && form[field] !== "";
@@ -44,12 +44,18 @@ const { openCrudFormDialog } = useFormWrapper();
const settingStore = useSettingStore();
const pluginStore = usePluginStore();
function createInputColumn(inputDefine: any) {
function createInputColumn(field: string, inputDefine: any) {
const form = cloneDeep(inputDefine);
useReference(form);
delete form.value;
delete form.rules;
form.required = false;
if (field === "challengeType") {
form.component = {
...form.component,
options: [{ value: "auto", label: "自动匹配" }],
};
}
if (form.component) {
form.component.allowClear = true;
}
@@ -62,7 +68,11 @@ function createInputColumn(inputDefine: any) {
function createColumns(inputDefines: any) {
const columns: any = {};
for (const field of batchUpdateFields) {
columns[field] = createInputColumn(inputDefines[field]);
const inputDefine = inputDefines[field];
if (inputDefine == null) {
continue;
}
columns[field] = createInputColumn(field, inputDefine);
}
return columns;
}
@@ -74,7 +74,7 @@ export class CertApplyTemplateService extends BaseService<CertApplyTemplateEntit
}
private async getTemplateParams(req: ResolveApplyTemplateReq) {
if (!req.templateId) {
if (req.templateId == null) {
return {};
}
const template = await this.getTemplateById(req.templateId, req.userId, req.projectId);
@@ -85,6 +85,10 @@ export class CertApplyTemplateService extends BaseService<CertApplyTemplateEntit
}
private async getTemplateById(id: number, userId: number, projectId?: number) {
if (id === 0) {
//获取默认模版
return await this.getDefault(userId, projectId);
}
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
const template = await this.repository.findOne({
where: {
@@ -1,5 +1,5 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { CodeException, Constants } from "@certd/lib-server";
import { AccessService, CodeException, Constants, TextedException } from "@certd/lib-server";
import { CertInfoEntity } from "../entity/cert-info.js";
import { utils } from "@certd/basic";
import { PipelineService } from "../../pipeline/service/pipeline-service.js";
@@ -30,6 +30,9 @@ export class CertInfoFacade {
@Inject()
certApplyTemplateService: CertApplyTemplateService;
@Inject()
accessService: AccessService;
async getCertInfo(req: { domains?: string; certId?: number; userId: number; projectId: number; autoApply?: boolean; format?: string; autoApplyTemplateId?: number; autoApplyParams?: CertApplyTemplateParams }) {
const { domains, certId, userId, projectId } = req;
if (certId) {
@@ -129,11 +132,6 @@ export class CertInfoFacade {
}
}
const userEmailSetting = await this.userSettingsService.getSetting<UserEmailSetting>(req.userId, null, UserEmailSetting);
if (!userEmailSetting.list) {
throw new CodeException(Constants.res.openEmailNotFound);
}
const email = userEmailSetting.list[0];
const applyParams = await this.certApplyTemplateService.resolveApplyParams({
userId: req.userId,
projectId: req.projectId,
@@ -141,9 +139,30 @@ export class CertInfoFacade {
params: req.autoApplyParams,
});
if (!applyParams.email) {
let email = null;
const userEmailSetting = await this.userSettingsService.getSetting<UserEmailSetting>(req.userId, null, UserEmailSetting);
if (userEmailSetting.list && userEmailSetting.list.length > 0) {
email = userEmailSetting.list[0];
applyParams.email = email;
}
}
if (!applyParams.acmeAccountAccessId) {
//如果没有配置acmeAccountAccessId,则默认使用默认的acmeAccountAccessId
applyParams.sslProvider = "letsencrypt";
const defaultAccount = await this.accessService.getDefaultByType({type:"acmeAccount",userId:req.userId,projectId:req.projectId,subtype:"letsencrypt"});
if (defaultAccount) {
applyParams.acmeAccountAccessId = defaultAccount.id;
}
}
if (applyParams.certApplyRetryCount == null) {
//如果没有配置重试次数,则默认再重试2次
applyParams.certApplyRetryCount = 2;
}
return await this.pipelineService.createAutoPipeline({
domains: req.domains,
email,
projectId: req.projectId,
userId: req.userId,
from: "OpenAPI",
@@ -168,7 +187,7 @@ export class CertInfoFacade {
await utils.sleep(2000);
}
const certInfo = await this.certInfoService.getByPipelineId(req.pipelineId);
throw new CodeException({
throw new TextedException({
...Constants.res.openCertApplying,
data: {
pipelineId: req.pipelineId,
@@ -176,7 +195,6 @@ export class CertInfoFacade {
},
});
}
private canRetryErrorPipeline(updateTime?: Date) {
if (!updateTime) {
return false;
@@ -79,6 +79,44 @@ describe("pipeline batch update", () => {
});
});
it("updates automatic domain verification, issuer and ACME account", () => {
const pipeline: any = {
stages: [
{
tasks: [
{
steps: [
{
type: "CertApply",
input: {
challengeType: "dns",
sslProvider: "letsencrypt",
acmeAccountAccessId: 1,
renewDays: 20,
},
},
],
},
],
},
],
};
updateCertApplyStepInputs(pipeline, {
challengeType: "auto",
sslProvider: "google",
acmeAccountAccessId: 2,
renewDays: 10,
});
assert.deepEqual(pipeline.stages[0].tasks[0].steps[0].input, {
challengeType: "auto",
sslProvider: "google",
acmeAccountAccessId: 2,
renewDays: 10,
});
});
it("updates uploaded cert pipelines only for fields defined by the plugin", () => {
const pipeline: any = {
stages: [
@@ -1,4 +1,7 @@
export type CertApplyStepInputPatch = {
challengeType?: "auto";
sslProvider?: string;
acmeAccountAccessId?: number;
renewDays?: number;
privateKeyType?: string;
};
@@ -30,7 +33,7 @@ function applyPatchFields(target: Record<string, unknown>, patch: CertApplyStepI
}
export function updateCertApplyStepInputs(pipeline: any, patch: CertApplyStepInputPatch, getStepInputDefine?: GetStepInputDefine) {
const fields: (keyof CertApplyStepInputPatch)[] = ["renewDays", "privateKeyType"];
const fields: (keyof CertApplyStepInputPatch)[] = ["challengeType", "sslProvider", "acmeAccountAccessId", "renewDays", "privateKeyType"];
let count = 0;
for (const stage of pipeline?.stages || []) {
for (const task of stage?.tasks || []) {
@@ -1303,11 +1303,18 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
async createAutoPipeline(req: { domains: string[]; email: string; userId: number; projectId?: number; from: string; applyParams?: CertApplyTemplateParams }) {
async createAutoPipeline(req: { domains: string[]; userId: number; projectId?: number; from: string; applyParams?: CertApplyTemplateParams }) {
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
const applyParams:any = {
...req.applyParams,
domains: req.domains,
}
if (!applyParams.challengeType) {
applyParams.challengeType = "auto";
}
const pipeline: any = {
title: req.domains[0] + `证书自动申请【${req.from ?? "OpenAPI"}`,
runnableType: "pipeline",
@@ -1351,17 +1358,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
sslProvider: "letsencrypt",
privateKeyType: "rsa_2048",
certProfile: "classic",
preferredChain: "ISRG Root X1",
preferredChain: "ISRG Root X2",
useProxy: false,
skipLocalVerify: false,
maxCheckRetryCount: 20,
waitDnsDiffuseTime: 30,
pfxArgs: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
successNotify: true,
...req.applyParams,
domains: req.domains,
email: req.email,
challengeType: "auto",
...applyParams,
},
strategy: {
runStrategy: 0, // 正常执行
@@ -555,7 +555,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
@TaskInput({
title: "证书申请失败重试次数",
value: 0,
value: 1,
component: {
name: "a-input-number",
vModel: "value",
@@ -565,7 +565,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
maybeNeed: true,
helper: "证书申请失败后,等待30秒再自动重试;0表示不重试",
})
certApplyRetryCount = 0;
certApplyRetryCount?: number;
acme!: AcmeService;
@@ -706,7 +706,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
}
private getCertApplyRetryCount() {
const retryCount = Number(this.certApplyRetryCount);
const retryCount = Number(this.certApplyRetryCount || 1);
if (!Number.isFinite(retryCount) || retryCount <= 0) {
return 0;
}