Merge branch 'codex/v2-persist-01' into v2-invite

This commit is contained in:
xiaojunnuo
2026-05-24 19:48:24 +08:00
125 changed files with 4108 additions and 27885 deletions
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/publishlab/node-acme-client/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/acme-client
## [1.40.2](https://github.com/publishlab/node-acme-client/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/acme-client
## [1.40.1](https://github.com/publishlab/node-acme-client/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/acme-client
+3 -3
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.40.1",
"@certd/basic": "^1.40.3",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -76,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+4
View File
@@ -467,6 +467,10 @@ class AcmeClient {
return createHash('sha256').update(result).digest('base64url');
}
if (challenge.type === 'dns-persist-01') {
return '';
}
/* https://datatracker.ietf.org/doc/html/rfc8737 */
if (challenge.type === 'tls-alpn-01') {
return result;
+5 -1
View File
@@ -97,7 +97,11 @@ export interface DnsChallenge extends ChallengeAbstract {
token: string;
}
export type Challenge = HttpChallenge | DnsChallenge;
export interface DnsPersistChallenge extends ChallengeAbstract {
type: "dns-persist-01";
}
export type Challenge = HttpChallenge | DnsChallenge | DnsPersistChallenge;
/**
* Certificate
+20 -1
View File
@@ -170,7 +170,7 @@ export function createChallengeFn(opts = {}) {
if (txtRecords.length === 0) {
throw new Error(`没有找到TXT解析记录(${recordName}`);
throw new Error(`没有找到TXT解析记录(${recordName},请稍后重试`);
}
return txtRecords;
}
@@ -203,6 +203,24 @@ export function createChallengeFn(opts = {}) {
return true;
}
async function verifyDnsPersistChallenge(authz, challenge, keyAuthorization, prefix = '_validation-persist.') {
const recordName = `${prefix}${authz.identifier.value.replace(/^\*\./, '')}`;
log(`本地校验DNS持久验证TXT记录: ${recordName}`);
let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
recordValues = [...new Set(recordValues)];
const expected = challenge.expectedRecordValue;
if (!expected) {
log(`未提供dns-persist-01本地校验期望值,跳过精确匹配,仅确认TXT记录存在`);
return true;
}
log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
if (!recordValues.length || !recordValues.includes(expected)) {
throw new Error(`没有找到需要的DNS持久验证TXT记录: ${recordName},请稍后重试,期望:${expected},结果:${recordValues}`);
}
log(`DNS持久验证记录匹配成功(${challenge.type}/${recordName}:${expected}`);
return true;
}
/**
* Verify ACME TLS ALPN challenge
*
@@ -234,6 +252,7 @@ export function createChallengeFn(opts = {}) {
challenges: {
'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge,
'dns-persist-01': verifyDnsPersistChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge,
},
walkTxtRecord,
+1 -1
View File
@@ -57,7 +57,7 @@ export interface ClientExternalAccountBindingOptions {
export interface ClientAutoOptions {
csr: CsrBuffer | CsrString;
challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise<string>) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string}>;
challengeCreateFn: (authz: Authorization, keyAuthorization: (challenge:rfc8555.Challenge)=>Promise<string>) => Promise<{recordReq?:any,recordRes?:any,dnsProvider?:any,challenge: rfc8555.Challenge,keyAuthorization:string,httpUploader?:any}>;
challengeRemoveFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string,recordReq:any, recordRes:any,dnsProvider:any,httpUploader:any) => Promise<any>;
email?: string;
termsOfServiceAgreed?: boolean;
+5 -1
View File
@@ -97,7 +97,11 @@ export interface DnsChallenge extends ChallengeAbstract {
token: string;
}
export type Challenge = HttpChallenge | DnsChallenge;
export interface DnsPersistChallenge extends ChallengeAbstract {
type: 'dns-persist-01';
}
export type Challenge = HttpChallenge | DnsChallenge | DnsPersistChallenge;
/**
* Certificate
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/basic
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/basic
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/basic
+1 -1
View File
@@ -1 +1 @@
00:34
22:57
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -52,5 +52,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/pipeline
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/pipeline
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/pipeline
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -19,8 +19,8 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.40.1",
"@certd/plus-core": "^1.40.1",
"@certd/basic": "^1.40.3",
"@certd/plus-core": "^1.40.3",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -49,5 +49,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+1
View File
@@ -19,6 +19,7 @@ export type AccessInputDefine = FormItemProps & {
};
export type AccessDefine = Registrable & {
icon?: string;
subtype?: string;
input?: {
[key: string]: AccessInputDefine;
};
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/lib-huawei
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/lib-huawei
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/lib-huawei
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -27,5 +27,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/lib-iframe
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/lib-iframe
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/lib-iframe
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -34,5 +34,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/jdcloud
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/jdcloud
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/jdcloud
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.40.1",
"version": "1.40.3",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -59,5 +59,5 @@
"fetch"
]
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/lib-k8s
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/lib-k8s
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/lib-k8s
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -19,7 +19,7 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.40.1",
"@certd/basic": "^1.40.3",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
@@ -36,5 +36,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/lib-server
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/lib-server
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/lib-server
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.40.1",
"version": "1.40.3",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.40.1",
"@certd/basic": "^1.40.1",
"@certd/pipeline": "^1.40.1",
"@certd/plugin-lib": "^1.40.1",
"@certd/plus-core": "^1.40.1",
"@certd/acme-client": "^1.40.3",
"@certd/basic": "^1.40.3",
"@certd/pipeline": "^1.40.3",
"@certd/plugin-lib": "^1.40.3",
"@certd/plus-core": "^1.40.3",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
@@ -19,6 +19,9 @@ export class AccessEntity {
@Column({ comment: '类型', length: 100 })
type: string;
@Column({ name: 'subtype', comment: '子类型', length: 100, nullable: true })
subtype: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
setting: string;
@@ -1,4 +1,4 @@
import { IAccessService } from '@certd/pipeline';
import { IAccessService } from "@certd/pipeline";
export class AccessGetter implements IAccessService {
userId: number;
@@ -15,6 +15,6 @@ export class AccessGetter implements IAccessService {
}
async getCommonById<T = any>(id: any) {
return await this.getter<T>(id, 0,null);
return await this.getter<T>(id, 0, null);
}
}
@@ -1,14 +1,16 @@
import assert from "assert";
import esmock from "esmock";
import { AccessService } from "./access-service.js";
describe("AccessService", () => {
it("does not write id into access setting when updating selected fields", async () => {
let updateParam: any;
const service = new AccessService();
service.info = async () => ({
id: 12,
type: "eab",
} as any);
service.info = async () =>
({
id: 12,
type: "eab",
}) as any;
service.decryptAccessEntity = () => ({
kid: "kid-1",
});
@@ -27,4 +29,82 @@ describe("AccessService", () => {
accountKey: "account-key",
});
});
it("writes subtype from access define field", async () => {
const { AccessService: MockedAccessService } = await esmock("./access-service.js", {
"@certd/pipeline": {
accessRegistry: {
getDefine(type: string) {
assert.equal(type, "acmeAccount");
return {
name: "acmeAccount",
subtype: "caType",
input: {
caType: {},
account: {
encrypt: true,
},
},
};
},
},
},
});
const service = new MockedAccessService();
service.encryptService = {
encrypt(value: string) {
return `encrypted:${value}`;
},
};
const param: any = {
type: "acmeAccount",
setting: JSON.stringify({
caType: "letsencrypt",
account: JSON.stringify({
accountKey: "key",
accountUri: "https://example.com/acct/1",
caType: "letsencrypt",
}),
}),
};
service.encryptSetting(param);
assert.equal(param.subtype, "letsencrypt");
});
it("allows acme account access to be saved before account generation", async () => {
const { AccessService: MockedAccessService } = await esmock("./access-service.js", {
"@certd/pipeline": {
accessRegistry: {
getDefine() {
return {
name: "acmeAccount",
subtype: "caType",
input: {
caType: {},
account: {
encrypt: true,
},
},
};
},
},
},
});
const service = new MockedAccessService();
const param: any = {
type: "acmeAccount",
setting: JSON.stringify({
caType: "letsencrypt",
}),
};
service.encryptSetting(param);
assert.equal(param.subtype, "letsencrypt");
assert.deepEqual(JSON.parse(param.setting), {
caType: "letsencrypt",
});
});
});
@@ -1,17 +1,17 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import {AccessGetter, BaseService, PageReq, PermissionException, ValidateException} from '../../../index.js';
import {AccessEntity} from '../entity/access.js';
import {AccessDefine, accessRegistry, newAccess} from '@certd/pipeline';
import {EncryptService} from './encrypt-service.js';
import { logger, utils } from '@certd/basic';
import { AccessGetter, BaseService, PageReq, PermissionException, ValidateException } from "../../../index.js";
import { AccessEntity } from "../entity/access.js";
import { AccessDefine, accessRegistry, newAccess } from "@certd/pipeline";
import { EncryptService } from "./encrypt-service.js";
import { logger, utils } from "@certd/basic";
/**
* 授权
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AccessService extends BaseService<AccessEntity> {
@InjectEntityModel(AccessEntity)
repository: Repository<AccessEntity>;
@@ -36,16 +36,16 @@ export class AccessService extends BaseService<AccessEntity> {
async add(param) {
let oldEntity = null;
if (param._copyFrom){
if (param._copyFrom) {
oldEntity = await this.info(param._copyFrom);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
}
if (oldEntity.userId !== param.userId) {
throw new ValidateException('您无权查看该授权配置');
if (oldEntity.userId !== param.userId) {
throw new ValidateException("您无权查看该授权配置");
}
}
delete param._copyFrom
delete param._copyFrom;
this.encryptSetting(param, oldEntity);
param.keyId = "ac_" + utils.id.simpleNanoId();
return await super.add(param);
@@ -62,17 +62,20 @@ export class AccessService extends BaseService<AccessEntity> {
return;
}
const json = JSON.parse(setting);
if (accessDefine.subtype) {
param.subtype = json[accessDefine.subtype] || null;
}
let oldSetting = {};
let encryptSetting = {};
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === '{}';
const firstEncrypt = !oldSettingEntity || !oldSettingEntity.encryptSetting || oldSettingEntity.encryptSetting === "{}";
if (oldSettingEntity) {
oldSetting = JSON.parse(oldSettingEntity.setting || '{}');
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || '{}');
oldSetting = JSON.parse(oldSettingEntity.setting || "{}");
encryptSetting = JSON.parse(oldSettingEntity.encryptSetting || "{}");
}
for (const key in json) {
//加密
let value = json[key];
if (value && typeof value === 'string') {
if (value && typeof value === "string") {
//去除前后空格
value = value.trim();
json[key] = value;
@@ -81,7 +84,7 @@ export class AccessService extends BaseService<AccessEntity> {
if (!accessInputDefine) {
continue;
}
if (!accessInputDefine.encrypt || !value || typeof value !== 'string') {
if (!accessInputDefine.encrypt || !value || typeof value !== "string") {
//定义无需加密、value为空、不是字符串 这些不需要加密
encryptSetting[key] = {
value: value,
@@ -96,7 +99,7 @@ export class AccessService extends BaseService<AccessEntity> {
const subIndex = Math.min(2, length);
let starLength = length - subIndex * 2;
starLength = Math.max(2, starLength);
const starString = '*'.repeat(starLength);
const starString = "*".repeat(starLength);
json[key] = value.substring(0, subIndex) + starString + value.substring(value.length - subIndex);
encryptSetting[key] = {
value: this.encryptService.encrypt(value),
@@ -116,21 +119,21 @@ export class AccessService extends BaseService<AccessEntity> {
async update(param) {
const oldEntity = await this.info(param.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
}
this.encryptSetting(param, oldEntity);
delete param.keyId
delete param.keyId;
return await super.update(param);
}
async updateAccess(access: any) {
const oldEntity = await this.info(access.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
}
const setting = this.decryptAccessEntity(oldEntity);
for (const key of Object.keys(access)) {
if (key === 'id') {
if (key === "id") {
continue;
}
setting[key] = access[key];
@@ -145,11 +148,13 @@ export class AccessService extends BaseService<AccessEntity> {
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
throw new ValidateException("该授权配置不存在,请确认是否已被删除");
}
return {
id: entity.id,
name: entity.name,
type: entity.type,
subtype: entity.subtype,
userId: entity.userId,
projectId: entity.projectId,
};
@@ -162,14 +167,14 @@ export class AccessService extends BaseService<AccessEntity> {
}
if (checkUserId) {
if (userId == null) {
throw new ValidateException('userId不能为空');
throw new ValidateException("userId不能为空");
}
if (userId !== entity.userId) {
throw new PermissionException('您对该Access授权无访问权限');
throw new PermissionException("您对该Access授权无访问权限");
}
}
if (projectId != null && projectId !== entity.projectId) {
throw new PermissionException('您对该Access授权无访问权限');
throw new PermissionException("您对该Access授权无访问权限");
}
// const access = accessRegistry.get(entity.type);
@@ -178,8 +183,8 @@ export class AccessService extends BaseService<AccessEntity> {
id: entity.id,
...setting,
};
const accessGetter = new AccessGetter(userId,projectId, this.getById.bind(this));
return await newAccess(entity.type, input,accessGetter);
const accessGetter = new AccessGetter(userId, projectId, this.getById.bind(this));
return await newAccess(entity.type, input, accessGetter);
}
async getById(id: any, userId: number, projectId?: number): Promise<any> {
@@ -188,7 +193,7 @@ export class AccessService extends BaseService<AccessEntity> {
decryptAccessEntity(entity: AccessEntity): any {
let setting = {};
if (entity.encryptSetting && entity.encryptSetting !== '{}') {
if (entity.encryptSetting && entity.encryptSetting !== "{}") {
setting = JSON.parse(entity.encryptSetting);
for (const key in setting) {
//解密
@@ -213,12 +218,11 @@ export class AccessService extends BaseService<AccessEntity> {
return accessRegistry.getDefine(type);
}
async getSimpleByIds(ids: number[], userId: any, projectId?: number) {
if (ids.length === 0) {
return [];
}
if (userId==null) {
if (userId == null) {
return [];
}
return await this.repository.find({
@@ -231,24 +235,24 @@ export class AccessService extends BaseService<AccessEntity> {
id: true,
name: true,
type: true,
userId:true,
projectId:true,
subtype: true,
userId: true,
projectId: true,
},
});
}
/**
* 复制授权到其他项目
* @param accessId
* @param projectId
* @param accessId
* @param projectId
*/
async copyTo(accessId: number,projectId?: number) {
async copyTo(accessId: number, projectId?: number) {
const access = await this.info(accessId);
if (access == null) {
throw new Error(`该授权配置不存在,请确认是否已被删除:id=${accessId}`);
}
const keyId = access.keyId;
//检查目标项目里是否已经有相同keyId的配置
const existAccess = await this.repository.findOne({
@@ -263,10 +267,10 @@ export class AccessService extends BaseService<AccessEntity> {
}
const newAccess = {
...access,
userId:-1,
userId: -1,
id: undefined,
projectId,
}
};
await this.repository.save(newAccess);
return newAccess.id;
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/midway-flyway-js
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.40.1",
"version": "1.40.3",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -49,5 +49,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/plugin-cert
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/plugin-cert
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/plugin-cert
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -18,10 +18,10 @@
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.40.1",
"@certd/basic": "^1.40.1",
"@certd/pipeline": "^1.40.1",
"@certd/plugin-lib": "^1.40.1",
"@certd/acme-client": "^1.40.3",
"@certd/basic": "^1.40.3",
"@certd/pipeline": "^1.40.3",
"@certd/plugin-lib": "^1.40.3",
"psl": "^1.9.0",
"punycode.js": "^2.3.1"
},
@@ -41,5 +41,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
**Note:** Version bump only for package @certd/plugin-lib
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/plugin-lib
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
**Note:** Version bump only for package @certd/plugin-lib
+6 -6
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.40.1",
"version": "1.40.3",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -23,10 +23,10 @@
"@alicloud/pop-core": "^1.7.10",
"@alicloud/tea-util": "^1.4.11",
"@aws-sdk/client-s3": "^3.964.0",
"@certd/acme-client": "^1.40.1",
"@certd/basic": "^1.40.1",
"@certd/pipeline": "^1.40.1",
"@certd/plus-core": "^1.40.1",
"@certd/acme-client": "^1.40.3",
"@certd/basic": "^1.40.3",
"@certd/pipeline": "^1.40.3",
"@certd/plus-core": "^1.40.3",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -61,5 +61,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "73996f055bbff996ee776e7788e5a5cb500fc197"
"gitHead": "01c91ba294f88bd07fddf9358c4301bbb4027916"
}
@@ -13,7 +13,7 @@ export interface ICertInfoGetter {
export type CertInfo = {
crt: string; //fullchain证书
key: string; //私钥
csr: string; //csr
csr?: string; //csr
oc?: string; //仅证书,非fullchain证书
ic?: string; //中间证书
pfx?: string;
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
### Bug Fixes
* 修复暗黑模式下注册页面验证码看不清的问题 ([5ba33be](https://github.com/certd/certd/commit/5ba33be30f765f06cafbfcc04f5e25320db01449))
### Performance Improvements
* 修复商业版套餐添加和修改时的字段显示 ([fb5b00d](https://github.com/certd/certd/commit/fb5b00d73f925036a65ce5003c57c1199578c34d))
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
**Note:** Version bump only for package @certd/ui-client
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
### Performance Improvements
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.40.1",
"version": "1.40.3",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -106,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.40.1",
"@certd/pipeline": "^1.40.1",
"@certd/lib-iframe": "^1.40.3",
"@certd/pipeline": "^1.40.3",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -54,6 +54,30 @@
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe735;</span>
<div class="name">wechat</div>
<div class="code-name">&amp;#xe735;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe667;</span>
<div class="name">QQ</div>
<div class="code-name">&amp;#xe667;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60c;</span>
<div class="name">wechat</div>
<div class="code-name">&amp;#xe60c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe609;</span>
<div class="name">acepanel</div>
<div class="code-name">&amp;#xe609;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60a;</span>
<div class="name">飞牛</div>
@@ -252,7 +276,7 @@
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.svg?t=1766772710945#iconfont') format('svg');
src: url('iconfont.svg?t=1779270521617#iconfont') format('svg');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
@@ -278,6 +302,42 @@
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-wxpay"></span>
<div class="name">
wechat
</div>
<div class="code-name">.icon-wxpay
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-qq"></span>
<div class="name">
QQ
</div>
<div class="code-name">.icon-qq
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-wechat"></span>
<div class="name">
wechat
</div>
<div class="code-name">.icon-wechat
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-acepanel"></span>
<div class="name">
acepanel
</div>
<div class="code-name">.icon-acepanel
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-fnos"></span>
<div class="name">
@@ -575,6 +635,38 @@
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-wxpay"></use>
</svg>
<div class="name">wechat</div>
<div class="code-name">#icon-wxpay</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-qq"></use>
</svg>
<div class="name">QQ</div>
<div class="code-name">#icon-qq</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-wechat"></use>
</svg>
<div class="name">wechat</div>
<div class="code-name">#icon-wechat</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-acepanel"></use>
</svg>
<div class="name">acepanel</div>
<div class="code-name">#icon-acepanel</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-fnos"></use>
@@ -1,6 +1,6 @@
@font-face {
font-family: "iconfont"; /* Project id 4688792 */
src: url('iconfont.svg?t=1766772710945#iconfont') format('svg');
src: url('iconfont.svg?t=1779270521617#iconfont') format('svg');
}
.iconfont {
@@ -11,6 +11,22 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-wxpay:before {
content: "\e735";
}
.icon-qq:before {
content: "\e667";
}
.icon-wechat:before {
content: "\e60c";
}
.icon-acepanel:before {
content: "\e609";
}
.icon-fnos:before {
content: "\e60a";
}
File diff suppressed because one or more lines are too long
@@ -5,6 +5,34 @@
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "5466096",
"name": "wechat",
"font_class": "wxpay",
"unicode": "e735",
"unicode_decimal": 59189
},
{
"icon_id": "9186714",
"name": "QQ",
"font_class": "qq",
"unicode": "e667",
"unicode_decimal": 58983
},
{
"icon_id": "26267577",
"name": "wechat",
"font_class": "wechat",
"unicode": "e60c",
"unicode_decimal": 58892
},
{
"icon_id": "37557578",
"name": "acepanel",
"font_class": "acepanel",
"unicode": "e609",
"unicode_decimal": 58889
},
{
"icon_id": "45984300",
"name": "飞牛",
@@ -14,6 +14,14 @@
/>
<missing-glyph />
<glyph glyph-name="wxpay" unicode="&#59189;" d="M410.493712 251.773712c-64.448471-36.97706-74.006881 20.759958-74.006881 20.759958l-80.772173 193.983933c-31.078562 92.178305 26.897497 41.56191 26.897498 41.561909s49.746372-38.732181 87.50193-62.333712c37.732946-23.602608 80.745253-6.927882 80.745254-6.927882l528.043743 250.842313C881.479874 814.421333 720.547129 896 538.352656 896 241.013636 896 0 678.901232 0 411.080547c0-154.046856 79.806318-291.154103 204.11518-380.019214L181.698086-101.56551s-10.92805-38.720336 26.945952-20.759958c25.808892 12.243853 91.603314 56.122953 130.768353 82.82771 61.570288-22.083298 128.651441-34.345455 198.970414-34.345455 297.315331 0 538.378498 217.098768 538.378499 484.924837 0 77.573115-20.313102 150.8338-56.295235 215.861568-168.236416-104.176656-559.545472-346.282128-609.973434-375.167327z" horiz-adv-x="1076" />
<glyph glyph-name="qq" unicode="&#58983;" d="M511.09761-61.257c-80.159 0-153.737 25.019-201.11 62.386-24.057-6.702-54.831-17.489-74.252-30.864-16.617-11.439-14.546-23.106-11.55-27.816 13.15-20.689 225.583-13.211 286.912-6.767v3.061zM496.65061-61.257c80.157 0 153.737 25.019 201.11 62.386 24.057-6.702 54.83-17.489 74.253-30.864 16.616-11.439 14.543-23.106 11.55-27.816-13.15-20.689-225.584-13.211-286.914-6.767v3.061zM497.12861 421.476c131.934 0.876 237.669 25.783 273.497 35.34 8.541 2.28 13.11 6.364 13.11 6.364 0.03 1.172 0.542 20.952 0.542 31.155C784.27761 666.167 701.12561 838.827 496.64061 838.838 292.15661 838.827 209.00061 666.168 209.00061 494.335c0-10.203 0.516-29.983 0.547-31.155 0 0 3.717-3.821 10.529-5.67 33.078-8.98 140.803-35.139 276.08-36.034h0.972zM860.28261 276.218c-8.12 26.086-19.204 56.506-30.427 85.72 0 0-6.456 0.795-9.718-0.148-100.71-29.205-222.773-47.818-315.792-46.695h-0.962C410.88561 313.983 289.65061 332.383 189.27961 361.302 185.44461 362.405 177.87261 361.937 177.87261 361.937 166.64961 332.724 155.56661 302.304 147.44761 276.218 108.72961 151.832 121.27261 100.356 130.82461 99.202c20.496-2.474 79.78 93.637 79.78 93.637 0-97.66 88.324-247.617 290.576-248.996a718.01 718.01 0 0 0 5.367 0C708.80161-54.778 797.12261 95.178 797.12261 192.838c0 0 59.284-96.111 79.783-93.637 9.55 1.154 22.093 52.63-16.623 177.017M434.38261 579.083c-27.9-1.24-51.745 30.106-53.24 69.956-1.518 39.877 19.858 73.207 47.764 74.454 27.875 1.224 51.703-30.109 53.218-69.974 1.527-39.877-19.853-73.2-47.742-74.436m206.67 69.956c-1.494-39.85-25.34-71.194-53.24-69.956-27.888 1.238-49.269 34.559-47.742 74.435 1.513 39.868 25.341 71.201 53.216 69.974 27.909-1.247 49.285-34.576 47.767-74.453M683.94261 527.373c-7.323 17.609-81.062 37.227-172.353 37.227h-0.98c-91.29 0-165.031-19.618-172.352-37.227a6.244 6.244 0 0 1-0.535-2.505c0-1.269 0.393-2.414 1.006-3.386 6.168-9.765 88.054-58.018 171.882-58.018h0.98c83.827 0 165.71 48.25 171.881 58.016a6.352 6.352 0 0 1 1.002 3.395c0 0.897-0.2 1.736-0.531 2.498M467.63161 639.623c1.26-15.886-7.377-30-19.266-31.542-11.907-1.544-22.569 10.083-23.836 25.978-1.243 15.895 7.381 30.008 19.25 31.538 11.927 1.549 22.607-10.088 23.852-25.974m73.097-7.935c2.533 4.118 19.827 25.77 55.62 17.886 9.401-2.07 13.75-5.116 14.668-6.316 1.355-1.77 1.726-4.29 0.352-7.684-2.722-6.725-8.338-6.542-11.454-5.226-2.01 0.85-26.94 15.889-49.905-6.553-1.579-1.545-4.405-2.074-7.085-0.242-2.678 1.834-3.786 5.553-2.196 8.135M504.33261 311.505h-0.967c-63.568-0.752-140.646 7.504-215.286 21.92-6.391-36.262-10.25-81.838-6.936-136.196 8.37-137.384 91.62-223.736 220.118-224.996H506.48461c128.498 1.26 211.748 87.612 220.12 224.996 3.314 54.362-0.547 99.938-6.94 136.203-74.654-14.423-151.745-22.684-215.332-21.927M323.27461 318.984v-137.468s64.957-12.705 130.031-3.91V304.41c-41.225 2.262-85.688 7.304-130.031 14.574M788.09761 463.464s-121.98-40.387-283.743-41.539h-0.962c-161.497 1.147-283.328 41.401-283.744 41.539l-40.854-106.952c102.186-32.31 228.837-53.135 324.598-51.926l0.96 0.002c95.768-1.216 222.4 19.61 324.6 51.924l-40.855 106.952z" horiz-adv-x="1024" />
<glyph glyph-name="wechat" unicode="&#58892;" d="M1024.16 201.184c0 149.92-143.104 271.392-319.584 271.392-176.576 0-319.68-121.504-319.68-271.392S528-70.208 704.576-70.208c55.456 0 107.648 12.096 153.184 33.248l125.984-54.528-14.592 140.544c34.784 43.392 55.04 95.808 55.04 152.128zM596.832 274.72c-25.152 0-45.472 20.352-45.472 45.472s20.32 45.472 45.472 45.472c25.12 0 45.44-20.384 45.44-45.472s-20.384-45.472-45.44-45.472z m215.392 0c-25.056 0-45.44 20.352-45.44 45.472s20.384 45.472 45.44 45.472c25.184 0 45.536-20.384 45.536-45.472s-20.352-45.472-45.536-45.472zM704.576 508.512c49.376 0 96.416-8.8 139.264-24.64 0.32 5.728 0.992 11.232 0.992 16.992 0 198.08-189.152 358.624-422.432 358.624C189.184 859.488 0.032 698.976 0.032 500.864c0-74.496 26.816-143.776 72.704-201.12L53.472 114.08l166.432 72.096c41.216-19.2 86.784-32.16 134.88-38.784-3.616 17.504-5.824 35.424-5.824 53.792 0.032 169.44 159.552 307.296 355.616 307.296z m-139.808 209.6c33.184 0 60-26.88 60-60 0-33.184-26.816-60.064-60-60.064s-60.032 26.88-60.032 60.064c0 33.152 26.88 60 60.032 60zM280.032 598.048c-33.184 0-60 26.88-60 60.064 0 33.152 26.848 60 60 60 33.184 0 60.032-26.88 60.032-60s-26.88-60.064-60.032-60.064z" horiz-adv-x="1025" />
<glyph glyph-name="acepanel" unicode="&#58889;" d="M0 896m100.8867 0l822.2266 0q100.8867 0 100.8867-100.8867l0-822.2266q0-100.8867-100.8867-100.8867l-822.2266 0q-100.8867 0-100.8867 100.8867l0 822.2266q0 100.8867 100.8867 100.8867ZM469.123153 705.576355h171.507389l2.522167-29.004926 3.783252-30.26601 6.305418-55.487685 7.566503-69.359606 3.783251-29.004926 2.522167-23.960591 5.044335-42.876847 2.522168-26.482759 5.044335-40.35468 2.522167-23.960591 5.044335-44.137931 3.783252-32.788177 2.522167-21.438424v-6.305419h229.517241l-7.566502-27.743842-15.133005-50.44335-11.349754-39.093596-11.349753-39.093596-3.783252-10.08867h-23.960591l6.305419 6.305419 7.566503 8.827586v3.783251l-21.438424 16.394089h-3.783251l-8.827587-10.08867-11.349753-6.305419-8.827586-2.522167h-11.349754l-10.08867 3.783251-6.305419 7.566502-2.522167 7.566503v8.827586l1.261083 1.261084 36.571429 3.783251 25.221675 7.566503 13.871921 7.566502 8.827586 8.827586 5.044335 10.08867 2.522168 16.394089-2.522168 12.610837-6.305418 10.08867-8.827587 7.566503-15.133005 6.305418-5.044335 1.261084h-29.004926l-16.394088-5.044335-13.871922-7.566502-12.610837-11.349754-12.610837-18.916256-7.566503-20.17734-2.522167-17.655172v-17.655173l2.522167-15.133005 6.305419-15.133005 5.044335-6.305418v-2.522168h-49.182266l7.566502 10.08867 7.566503 11.349754v5.044335l-25.221675 11.349753-3.783251 1.261084-8.827587-12.610837-6.305418-6.305419-8.827587-3.783251h-8.827586l-10.08867 3.783251-5.044335 6.305419-2.522167 10.08867 2.522167 20.17734 5.044335 23.960591 5.044335 12.610837 6.305419 7.566503 7.566502 3.783251 6.305419 1.261084h8.827586l8.827587-3.783252 6.305418-10.08867 1.261084-3.783251 6.305419 1.261084 26.482758 10.08867-1.261083 10.08867-7.566503 12.610837-10.08867 7.566503-13.871921 6.305418-8.827586 1.261084h-25.221675l-15.133005-5.044335-13.871921-7.566502-12.610838-11.349754-8.827586-13.871921-6.305418-12.610838-3.783252-12.610837-2.522167-22.699507 1.261084-25.221675 6.305418-15.133005 6.305419-11.349754h-37.832512l-2.522168 16.394089-3.783251 45.399015-3.783251 35.310344-5.044335 59.270936H435.073892l6.305418 15.133005 18.916257 39.093596 10.088669 22.699508 10.08867 20.17734 6.305419 13.871921h66.837439l-1.261084 5.044335-5.044335 71.881773-2.522168 29.004926-3.783251 55.487685-6.305419 81.970443v11.349754h-8.827586l-2.522167-7.566502-20.17734-44.137931-20.17734-44.137932-22.699507-49.182266-23.960592-51.704433-20.17734-42.876847-10.088669-23.960591-12.610838-27.743843-10.08867-20.17734-10.08867-21.438423-10.08867-20.17734-15.133005-31.527094-22.699507-46.660098-13.871921-29.004926-15.133005-31.527094-13.871921-29.004926-1.261084-1.261084H139.980296l2.522167 7.566503 17.655172 35.310344 12.610838 23.960591 29.004926 56.748769 12.610837 23.960591 16.394089 32.788177 12.610838 23.960592 25.221674 49.182266 17.655173 34.049261 10.08867 18.916256 10.08867 20.17734 30.26601 59.270936 17.655172 34.049261 11.349754 21.438423 10.08867 20.17734 29.004926 56.748769 21.438423 41.615763 18.916257 36.571429 22.699507 44.137931zM809.615764 170.876847h13.871921l7.566502-3.783251 2.522168-3.783251v-11.349754l-6.305419-7.566502-12.610837-5.044335-22.699508-3.783252h-7.566502l2.522167 13.871922 6.305419 10.08867 10.08867 8.827586z" horiz-adv-x="1024" />
<glyph glyph-name="fnos" unicode="&#58890;" d="M144.482434 896h730.821577c67.983402-28.323585 116.141145-76.481328 144.46473-144.46473v-730.821577c-28.323585-67.983402-76.481328-116.141145-144.46473-144.46473h-730.821577c-67.983402 28.323585-116.141145 76.481328-144.46473 144.46473v730.821577c28.323585 67.983402 76.481328 116.141145 144.46473 144.46473zM229.461687 632.564315c-1.665593-32.445079 1.164216-66.43678 8.497926-101.975103 17.990108-19.800166 40.654075-29.717245 67.983402-29.742739-18.63595-17.208299-27.133876-38.453112-25.493776-63.73444-84.486373 1.053743-117.058921 43.543369-97.726141 127.46888a431.320697 431.320697 0 0 0 46.738589 67.983402zM781.826833 632.564315c66.938158-53.536929 75.436083-114.44156 25.493776-182.705394a3226.917178 3226.917178 0 0 0-399.40249-12.746888v-237.941909h50.987552c-9.883087 77.900481 24.108614 109.062373 101.975104 93.477179a275.647203 275.647203 0 0 0 4.248962 67.983402 80.458357 80.458357 0 0 0 21.244813 12.746888 571.281527 571.281527 0 0 0 169.958507-4.248962c-32.436581-40.280166-74.926207-60.105826-127.46888-59.485477 11.149278-79.387618-22.842423-107.719701-101.975104-84.979254a347.871071 347.871071 0 0 0-4.248962-76.481327 59.273029 59.273029 0 0 0-29.742739-21.244814 361.14483 361.14483 0 0 0-110.473029 0 59.273029 59.273029 0 0 0-29.742739 21.244814 814.381676 814.381676 0 0 0-12.746888 161.460581 814.381676 814.381676 0 0 0 12.746888 161.46058 305.415436 305.415436 0 0 0 38.240664 29.742739l365.410789 8.497925c15.576697 4.248963 25.493776 14.166041 29.742738 29.742739 5.761593 31.62078 4.34244 62.782672-4.248962 93.477178z" horiz-adv-x="1024" />
<glyph glyph-name="ksyun" unicode="&#59565;" d="M718.5408 442.1632c-4.1984 0-8.3968 0.7168-12.5952 0.7168a103.8336 103.8336 0 0 1-67.8912-25.088 139.6736 139.6736 0 0 1 13.6192 60.3136 133.12 133.12 0 0 1-2.4576 25.7024 139.5712 139.5712 0 0 1-274.432 0 133.12 133.12 0 0 1-2.4576-25.7024 139.6736 139.6736 0 0 1 13.6192-60.3136 103.8336 103.8336 0 0 1-67.8912 25.088c-4.1984 0-8.3968 0-12.5952-0.7168a104.5504 104.5504 0 1 1 101.0688-159.4368 116.736 116.736 0 0 1 6.144 11.0592 105.1648 105.1648 0 0 1 4.9152 76.6976 62.6688 62.6688 0 0 0 27.3408-20.48l2.6624-3.8912a83.2512 83.2512 0 0 0 133.9392 3.3792l4.7104-6.8608 51.2-73.9328a84.0704 84.0704 0 0 1 62.464-34.6112h5.5296a104.5504 104.5504 0 0 1 12.5952 208.384zM512 896a512 512 0 1 1 512-512A512 512 0 0 1 512 896z m200.8064-732.3648h-6.8608a152.4736 152.4736 0 0 0-120.5248 58.9824S509.2352 331.3664 508.0064 332.8a42.1888 42.1888 0 0 1-32.4608 15.2576 40.96 40.96 0 0 1-24.064-7.5776l122.88-175.4112a62.5664 62.5664 0 0 1 77.0048-20.48A97.5872 97.5872 0 0 0 593.92 112.4352a97.0752 97.0752 0 0 0-95.5392 39.1168l-49.4592 70.656a174.8992 174.8992 0 1 0-143.36 290.5088 209.7152 209.7152 0 0 0 413.9008 0 174.7968 174.7968 0 0 0-6.144-349.0816z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 94 KiB

@@ -1,5 +1,5 @@
<template>
<div class="flex">
<div class="flex captcha-image-input">
<a-input :value="valueRef" :placeholder="t('certd.captcha.inputImageCode')" autocomplete="off" @update:value="onChange">
<template #prefix>
<fs-icon icon="ion:image-outline"></fs-icon>
@@ -71,3 +71,10 @@ function emitChange(value: any) {
emit("change", value);
}
</script>
<style lang="less">
.captcha-image-input {
.input-right {
background-color: #cfcfcf;
}
}
</style>
@@ -11,14 +11,16 @@
<td class="record-value" :title="cnameRecord.recordValue">
<fs-copyable v-model="cnameRecord.recordValue"></fs-copyable>
</td>
<td class="status center flex-center">
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" :title="t('certd.verifyPlan.resetStatusTooltip')">
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
<td class="status center">
<span class="status-content">
<fs-values-format v-model="cnameRecord.status" :dict="statusDict" />
<a-tooltip v-if="cnameRecord.error" :title="cnameRecord.error">
<fs-icon class="ml-5 color-red" icon="ion:warning-outline"></fs-icon>
</a-tooltip>
<a-tooltip v-if="cnameRecord.status === 'valid'" :title="t('certd.verifyPlan.resetStatusTooltip')">
<fs-icon class="ml-2 color-yellow text-md pointer" icon="solar:undo-left-square-bold" @click="resetStatus"></fs-icon>
</a-tooltip>
</span>
</td>
<td class="center">
<template v-if="cnameRecord.status !== 'valid'">
@@ -142,5 +144,10 @@ async function resetStatus() {
.fs-copyable {
width: 100%;
}
.status-content {
display: inline-flex;
align-items: center;
justify-content: center;
}
}
</style>
@@ -2,11 +2,11 @@
<table class="cname-verify-plan">
<thead>
<tr>
<td style="width: 160px">{{ t("certd.verifyPlan.hostRecord") }}</td>
<td style="width: 100px; text-align: center">{{ t("certd.verifyPlan.recordType") }}</td>
<td style="width: 250px">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
<td style="width: 120px" class="center">{{ t("certd.status") }}</td>
<td style="width: 90px" class="center">{{ t("certd.verifyPlan.operation") }}</td>
<td class="col-host">{{ t("certd.verifyPlan.hostRecord") }}</td>
<td class="col-type center">{{ t("certd.verifyPlan.recordType") }}</td>
<td class="col-value">{{ t("certd.verifyPlan.setCnameRecord") }}</td>
<td class="col-status center">{{ t("certd.status") }}</td>
<td class="col-action center">{{ t("certd.verifyPlan.operation") }}</td>
</tr>
</thead>
<template v-for="key in domains" :key="key">
@@ -49,6 +49,21 @@ function onRecordChange(domain: string, record: CnameRecord) {
.cname-verify-plan {
width: 100%;
table-layout: fixed;
.col-host {
width: 220px;
}
.col-type {
width: 100px;
}
.col-value {
width: 360px;
}
.col-status {
width: 120px;
}
.col-action {
width: 150px;
}
tbody tr td {
border-top: 1px solid #e8e8e8 !important;
}
@@ -0,0 +1,144 @@
<template>
<tbody class="dns-persist-record-info">
<tr v-if="dnsPersistRecord">
<td class="host-record" :title="dnsPersistRecord.hostRecord">
<fs-copyable v-model="dnsPersistRecord.hostRecord"></fs-copyable>
</td>
<td style="text-align: center">TXT</td>
<td class="record-value" :title="dnsPersistRecord.recordValue">
<fs-copyable v-model="dnsPersistRecord.recordValue"></fs-copyable>
</td>
<td class="status center">
<fs-values-format v-model="dnsPersistRecord.status" :dict="statusDict" />
</td>
<td class="center">
<template v-if="dnsPersistRecord.status !== 'valid'">
<a-space>
<a-button type="primary" size="small" @click="openSettingDialog">设置TXT</a-button>
<a-button type="primary" size="small" :loading="loading" @click="doVerify">校验</a-button>
</a-space>
</template>
<div v-else class="helper">请勿删除TXT记录</div>
</td>
</tr>
<tr v-else>
<td colspan="5" class="color-red">{{ errorMessage || "请先选择ACME账号授权" }}</td>
</tr>
</tbody>
</template>
<script lang="ts" setup>
import { dict } from "@fast-crud/fast-crud";
import { message } from "ant-design-vue";
import { ref, watch } from "vue";
import { GetByDomain, Verify } from "/@/views/certd/cert/dns-persist/api";
import { useDnsPersistSettingDialog } from "/@/views/certd/cert/dns-persist/use-setting-dialog";
import { DnsPersistRecord } from "./type";
defineOptions({
name: "DnsPersistRecordInfo",
});
const props = defineProps<{
domain: string;
caType?: string;
acmeAccountAccessId?: number;
commonAcmeAccountAccessId?: number;
wildcard?: boolean;
persistUntil?: number;
}>();
const emit = defineEmits<{
change: [DnsPersistRecord];
}>();
const statusDict = dict({
data: [
{ value: "pending", label: "待设置", color: "warning" },
{ value: "validating", label: "校验中", color: "blue" },
{ value: "valid", label: "有效", color: "green" },
{ value: "failed", label: "请重试", color: "red" },
],
});
const dnsPersistRecord = ref<DnsPersistRecord | null>(null);
const loading = ref(false);
const errorMessage = ref("");
const { openDnsPersistSettingDialog } = useDnsPersistSettingDialog();
function onRecordChange() {
if (dnsPersistRecord.value) {
emit("change", dnsPersistRecord.value);
} else {
emit("change", {
domain: props.domain,
status: null,
} as any);
}
}
async function loadRecord() {
errorMessage.value = "";
dnsPersistRecord.value = null;
if (!props.domain || (!props.acmeAccountAccessId && !props.commonAcmeAccountAccessId)) {
onRecordChange();
return;
}
try {
dnsPersistRecord.value = await GetByDomain({
domain: props.domain,
caType: props.caType,
acmeAccountAccessId: props.acmeAccountAccessId,
commonAcmeAccountAccessId: props.commonAcmeAccountAccessId,
wildcard: props.wildcard,
persistUntil: props.persistUntil,
createOnNotFound: true,
});
onRecordChange();
} catch (e: any) {
errorMessage.value = e.message;
}
}
watch(
() => [props.domain, props.caType, props.acmeAccountAccessId, props.commonAcmeAccountAccessId, props.wildcard, props.persistUntil],
async () => {
await loadRecord();
},
{
immediate: true,
}
);
async function doVerify() {
if (!dnsPersistRecord.value?.id) {
return;
}
loading.value = true;
try {
const ok = await Verify(dnsPersistRecord.value.id);
message[ok ? "success" : "error"](ok ? "校验成功" : "未找到匹配的TXT记录,请稍后重试");
await loadRecord();
} finally {
loading.value = false;
}
}
function openSettingDialog() {
if (!dnsPersistRecord.value) {
return;
}
openDnsPersistSettingDialog({
record: dnsPersistRecord.value,
onDone: loadRecord,
});
}
</script>
<style lang="less">
.dns-persist-record-info {
.fs-copyable {
width: 100%;
}
}
</style>
@@ -0,0 +1,94 @@
<template>
<table class="dns-persist-verify-plan">
<thead>
<tr>
<td class="col-host">TXT主机名</td>
<td class="col-type center">记录类型</td>
<td class="col-value">请设置TXT记录验证成功以后不要删除</td>
<td class="col-status center">状态</td>
<td class="col-action center">操作</td>
</tr>
</thead>
<template v-for="key in domains" :key="key">
<dns-persist-record-info
:domain="key"
:ca-type="caType"
:acme-account-access-id="acmeAccountAccessId"
:common-acme-account-access-id="commonAcmeAccountAccessId"
:wildcard="modelValue[key]?.wildcard"
:persist-until="modelValue[key]?.persistUntil"
@change="onRecordChange(key, $event)"
/>
</template>
</table>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import DnsPersistRecordInfo from "./dns-persist-record-info.vue";
import { DnsPersistRecord } from "./type";
defineOptions({
name: "DnsPersistVerifyPlan",
});
const emit = defineEmits(["update:modelValue", "change"]);
const props = defineProps<{
modelValue: Record<string, DnsPersistRecord>;
caType?: string;
acmeAccountAccessId?: number;
commonAcmeAccountAccessId?: number;
}>();
const domains = computed(() => {
return Object.keys(props.modelValue || {});
});
function onRecordChange(domain: string, record: DnsPersistRecord) {
const value = { ...props.modelValue };
value[domain] = {
...value[domain],
...record,
};
emit("update:modelValue", value);
emit("change", value);
}
</script>
<style lang="less">
.dns-persist-verify-plan {
width: 100%;
table-layout: fixed;
.col-host {
width: 220px;
}
.col-type {
width: 100px;
}
.col-value {
width: 360px;
}
.col-status {
width: 120px;
}
.col-action {
width: 150px;
}
tbody tr td {
border-top: 1px solid #e8e8e8 !important;
}
tr {
td {
border: 0 !important;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.center {
text-align: center;
}
}
}
}
</style>
@@ -46,13 +46,28 @@
<div class="form-item">
<span class="label">{{ t("certd.verifyPlan.dnsAccess") }}:</span>
<span class="input">
<access-selector v-model="item.dnsProviderAccessId" size="small" :type="item.dnsProviderAccessType || item.dnsProviderType" :placeholder="t('certd.verifyPlan.pleaseSelect')" @change="onPlanChanged"></access-selector>
<access-selector
v-model="item.dnsProviderAccessId"
size="small"
:type="item.dnsProviderAccessType || item.dnsProviderType"
:placeholder="t('certd.verifyPlan.pleaseSelect')"
@change="onPlanChanged"
></access-selector>
</span>
</div>
</div>
<div v-if="item.type === 'cname'" class="plan-cname">
<cname-verify-plan v-model="item.cnameVerifyPlan" @change="onPlanChanged" />
</div>
<div v-if="item.type === 'dns-persist'" class="plan-dns-persist">
<dns-persist-verify-plan
v-model="item.dnsPersistVerifyPlan"
:ca-type="caType"
:acme-account-access-id="acmeAccountAccessId"
:common-acme-account-access-id="commonAcmeAccountAccessId"
@change="onPlanChanged"
/>
</div>
<div v-if="item.type === 'http'" class="plan-http">
<http-verify-plan v-model="item.httpVerifyPlan" @change="onPlanChanged" />
<div class="helper">{{ t("certd.verifyPlan.httpHelper") }}</div>
@@ -76,6 +91,7 @@ import { useI18n } from "vue-i18n";
import { dict, FsDictSelect } from "@fast-crud/fast-crud";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import CnameVerifyPlan from "./cname-verify-plan.vue";
import DnsPersistVerifyPlan from "./dns-persist-verify-plan.vue";
import HttpVerifyPlan from "./http-verify-plan.vue";
import { Form } from "ant-design-vue";
import { DomainsVerifyPlanInput } from "./type";
@@ -92,6 +108,10 @@ const challengeTypeOptions = ref<any[]>([
label: t("certd.verifyPlan.dnsChallenge"),
value: "dns",
},
{
label: "DNS持久验证",
value: "dns-persist",
},
{
label: t("certd.verifyPlan.cnameChallenge"),
value: "cname",
@@ -106,6 +126,9 @@ const props = defineProps<{
modelValue?: DomainsVerifyPlanInput;
domains?: string[];
defaultType?: string;
caType?: string;
acmeAccountAccessId?: number;
commonAcmeAccountAccessId?: number;
}>();
const emit = defineEmits<{
@@ -189,11 +212,15 @@ async function onDomainsChanged(domains: string[]) {
const cnameOrigin = planItem.cnameVerifyPlan;
const httpOrigin = planItem.httpVerifyPlan;
const dnsPersistOrigin = planItem.dnsPersistVerifyPlan;
planItem.cnameVerifyPlan = {};
planItem.httpVerifyPlan = {};
planItem.dnsPersistVerifyPlan = {};
const cnamePlan = planItem.cnameVerifyPlan;
const httpPlan = planItem.httpVerifyPlan;
const dnsPersistPlan = planItem.dnsPersistVerifyPlan;
for (const subDomain of domainGroupItem.keySubDomains) {
const wildcard = true;
if (!cnameOrigin[subDomain]) {
//@ts-ignore
planItem.cnameVerifyPlan[subDomain] = {
@@ -225,6 +252,19 @@ async function onDomainsChanged(domains: string[]) {
domain: subDomain,
};
}
if (!dnsPersistOrigin?.[subDomain]) {
//@ts-ignore
dnsPersistPlan[subDomain] = {
domain: subDomain,
wildcard,
};
} else {
dnsPersistPlan[subDomain] = {
...dnsPersistOrigin[subDomain],
wildcard,
};
}
}
for (const subDomain of Object.keys(cnamePlan)) {
@@ -238,6 +278,12 @@ async function onDomainsChanged(domains: string[]) {
delete httpPlan[subDomain];
}
}
for (const subDomain of Object.keys(dnsPersistPlan)) {
if (!domainGroupItem.keySubDomains.includes(subDomain)) {
delete dnsPersistPlan[subDomain];
}
}
}
for (const domain of Object.keys(planRef.value)) {
const mainDomains = Object.keys(domainGroups);
@@ -268,6 +314,7 @@ watch(
overflow-x: auto;
.fullscreen-modal {
display: none;
background-color: rgba(0, 0, 0, 0.42);
}
&.fullscreen {
@@ -7,15 +7,32 @@ export type HttpRecord = {
httpUploadRootDir: string;
};
export type DnsPersistRecord = {
id?: number;
domain: string;
mainDomain?: string;
status?: string;
hostRecord?: string;
recordValue?: string;
caType?: string;
acmeAccountAccessId?: number;
accountUri?: string;
wildcard?: boolean;
persistUntil?: number;
dnsProviderType?: string;
dnsProviderAccess?: number;
};
export type DomainVerifyPlanInput = {
domain: string;
domains: string[];
type: "cname" | "dns" | "http";
type: "cname" | "dns" | "http" | "dns-persist";
dnsProviderType?: string;
dnsProviderAccessType?: string;
dnsProviderAccessId?: number;
cnameVerifyPlan?: Record<string, CnameRecord>;
httpVerifyPlan?: Record<string, HttpRecord>;
dnsPersistVerifyPlan?: Record<string, DnsPersistRecord>;
};
export type DomainsVerifyPlanInput = {
[key: string]: DomainVerifyPlanInput;
@@ -46,6 +46,14 @@ function checkDomainVerifyPlan(rule: any, value: DomainsVerifyPlanInput) {
if (!value[domain].dnsProviderType || !value[domain].dnsProviderAccessId) {
throw new Error($t("certd.verifyPlan.errors.dnsProviderRequired", { domain }));
}
} else if (type === "dns-persist") {
const subDomains = Object.keys(value[domain].dnsPersistVerifyPlan || {});
for (const subDomain of subDomains) {
const plan = value[domain].dnsPersistVerifyPlan[subDomain];
if (plan.status !== "valid") {
throw new Error(`DNS持久验证记录(${subDomain})还未校验成功`);
}
}
}
}
return true;
@@ -1,8 +1,8 @@
<template>
<div class="refresh-input">
<div class="refresh-input-line">
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" allow-clear @update:value="emit('update:value', $event)"></a-input>
<fs-button :loading="loading" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
<a-input class="refresh-input-control" :value="value" :placeholder="placeholder" :allow-clear="!disabled" :disabled="disabled" @update:value="emit('update:value', $event)"></a-input>
<fs-button :loading="loading" :disabled="disabled" type="primary" :text="buttonText" :icon="icon" @click="doRefresh"></fs-button>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
@@ -25,6 +25,7 @@ type RefreshInputProps = ComponentPropsType & {
icon?: string;
placeholder?: string;
successMessage?: string;
disabled?: boolean;
};
const fromType: any = inject("getFromType");
@@ -49,6 +50,9 @@ const placeholder = computed(() => props.placeholder || "");
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
const doRefresh = async () => {
if (props.disabled) {
return;
}
if (loading.value) {
return;
}
@@ -5,6 +5,7 @@ function createChallengeTypeDict() {
return dict({
data: [
{ value: "dns", label: $t("certd.verifyPlan.dnsChallenge"), color: "green" },
{ value: "dns-persist", label: "DNS持久验证", color: "cyan" },
{ value: "cname", label: $t("certd.verifyPlan.cnameProxyChallenge"), color: "blue" },
{ value: "http", label: $t("certd.verifyPlan.httpChallenge"), color: "yellow" },
],
@@ -39,7 +40,12 @@ export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境" },
{ value: "google", label: "Google" },
{ value: "zerossl", label: "ZeroSSL" },
{ value: "sslcom", label: "SSL.com" },
{ value: "litessl", label: "litessl" },
{ value: "custom", label: "自定义ACME" },
],
}),
get challengeTypeDict() {
@@ -11,6 +11,7 @@ export default {
siteMonitor: "Site Certificate Monitor",
settings: "Settings",
accessManager: "Access Management",
dnsPersistRecord: "DNS Persist Records",
subDomain: "Subdomain Delegation Settings",
pipelineGroup: "Pipeline Group Management",
openKey: "Open API Key",
@@ -11,6 +11,7 @@ export default {
siteMonitor: "站点证书监控",
settings: "设置",
accessManager: "授权管理",
dnsPersistRecord: "DNS持久验证记录",
subDomain: "子域名托管设置",
pipelineGroup: "流水线分组管理",
openKey: "开放接口密钥",
@@ -186,6 +186,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.dnsPersistRecord",
name: "DnsPersistRecord",
path: "/certd/cert/dns-persist",
component: "/certd/cert/dns-persist/index.vue",
meta: {
icon: "ion:shield-half-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.subDomain",
name: "SubDomain",
@@ -12,6 +12,12 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { props, ctx, api } = context;
const lastResRef = ref();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
query.query = query.query || {};
if (props.subtype) {
query.query.subtype = props.subtype;
} else {
delete query.query.subtype;
}
return await context.api.GetList(query);
};
const editRequest = async (req: EditReq) => {
@@ -47,7 +53,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { myProjectDict } = useDicts();
const typeRef = ref("aliyun");
context.typeRef = typeRef;
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api);
const commonColumnsDefine = getCommonColumnDefine(crudExpose, typeRef, api, props.subtype);
commonColumnsDefine.type.form.component.disabled = true;
const projectStore = useProjectStore();
return {
@@ -21,6 +21,10 @@ export default defineComponent({
type: String, //user | sys
default: "user",
},
subtype: {
type: String,
default: "",
},
modelValue: {},
},
emits: ["update:modelValue"],
@@ -30,10 +34,17 @@ export default defineComponent({
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
// crud
function refreshSearch() {
const form: any = { type: props.type };
if (props.subtype) {
form.subtype = props.subtype;
}
crudExpose.setSearchFormData({ form, mergeForm: true });
crudExpose.doRefresh();
}
function onTypeChanged(value: any) {
context.typeRef.value = value;
crudExpose.setSearchFormData({ form: { type: value }, mergeForm: true });
crudExpose.doRefresh();
refreshSearch();
}
watch(
() => {
@@ -44,6 +55,14 @@ export default defineComponent({
onTypeChanged(value);
}
);
watch(
() => {
return props.subtype;
},
() => {
refreshSearch();
}
);
//
onMounted(() => {
onTypeChanged(props.type);
@@ -9,7 +9,7 @@
<a-form-item-rest v-if="chooseForm.show">
<a-modal v-model:open="chooseForm.show" title="选择授权提供者" width="900px" @ok="chooseForm.ok">
<div style="height: 400px; position: relative">
<cert-access-modal v-model="selectedId" :type="type" :from="from"></cert-access-modal>
<cert-access-modal v-model="selectedId" :type="type" :subtype="subtype" :from="from"></cert-access-modal>
</div>
</a-modal>
</a-form-item-rest>
@@ -35,6 +35,10 @@ export default defineComponent({
type: String,
default: "aliyun",
},
subtype: {
type: String,
default: "",
},
placeholder: {
type: String,
default: "请选择",
@@ -1,11 +1,11 @@
import { ColumnCompositionProps, dict } from "@fast-crud/fast-crud";
import { computed, provide, ref, toRef } from "vue";
import { provide, ref, toRef } from "vue";
import { useReference } from "/@/use/use-refrence";
import { forEach, get, merge, set } from "lodash-es";
import SecretPlainGetter from "/@/views/certd/access/access-selector/access/secret-plain-getter.vue";
import { utils } from "/@/utils";
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any, fixedSubtype?: string) {
provide("getFromType", api.from);
provide("accessApi", api);
provide("get:plugin:type", () => {
@@ -34,6 +34,13 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any, api: any) {
}
}
console.log('crudBinding.value[mode + "Form"].columns', columnsRef.value);
if (mode === "add" && define.subtype && fixedSubtype) {
form.access = form.access || {};
const subtypeKey = `access.${define.subtype}`;
if (get(form, subtypeKey) == null) {
set(form, subtypeKey, fixedSubtype);
}
}
forEach(define.input, (value: any, mapKey: any) => {
const key = "access." + mapKey;
const field = {
@@ -0,0 +1,83 @@
import { request } from "/src/api/service";
const apiPrefix = "/cert/dns-persist";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
export async function AddObj(obj: any) {
return await request({
url: apiPrefix + "/add",
method: "post",
data: obj,
});
}
export async function UpdateObj(obj: any) {
return await request({
url: apiPrefix + "/update",
method: "post",
data: obj,
});
}
export async function DelObj(id: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function BuildRecord(body: { domain: string; accountUri: string; wildcard?: boolean; persistUntil?: number }) {
return await request({
url: apiPrefix + "/build",
method: "post",
data: body,
});
}
export async function GetByDomain(body: { domain: string; caType?: string; acmeAccountAccessId?: number; commonAcmeAccountAccessId?: number; wildcard?: boolean; persistUntil?: number; createOnNotFound?: boolean }) {
return await request({
url: apiPrefix + "/getByDomain",
method: "post",
data: body,
});
}
export async function CheckRecord(body: { hostRecord: string; recordValue: string }) {
return await request({
url: apiPrefix + "/check",
method: "post",
data: body,
});
}
export async function Verify(id: number) {
return await request({
url: apiPrefix + "/verify",
method: "post",
data: { id },
});
}
export async function TriggerVerify(id: number) {
return await request({
url: apiPrefix + "/triggerVerify",
method: "post",
data: { id },
});
}
export async function CreateTxt(body: { id: number; dnsProviderType?: string; dnsProviderAccess?: number }) {
return await request({
url: apiPrefix + "/createTxt",
method: "post",
data: body,
});
}
@@ -0,0 +1,349 @@
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { message, Modal, notification } from "ant-design-vue";
import * as api from "./api";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { createAccessApi } from "/@/views/certd/access/api";
import { useDnsPersistSettingDialog } from "./use-setting-dialog";
function parseAccount(account: any) {
if (!account) {
return null;
}
if (typeof account === "string") {
return JSON.parse(account);
}
return account;
}
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const accessApi = createAccessApi();
const { openDnsPersistSettingDialog } = useDnsPersistSettingDialog();
const accessDict = dict({
value: "id",
label: "name",
url: "accessDict",
async getNodesByValues(ids: number[]) {
return await accessApi.GetDictByIds(ids);
},
});
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
});
const statusDict = dict({
data: [
{ value: "pending", label: "待设置", color: "warning" },
{ value: "created", label: "已创建", color: "blue" },
{ value: "validating", label: "校验中", color: "blue" },
{ value: "valid", label: "有效", color: "green" },
{ value: "failed", label: "请重试", color: "red" },
],
});
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
const res = await api.DelObj(row.id);
if (res?.message) {
notification.warning({
message: "请到供应商删除TXT记录",
description: res.message,
duration: 0,
});
}
return res;
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
async function fillRecord(form: any) {
if (!form.domain || !form.acmeAccountAccessId) {
return;
}
const access: any = await accessApi.GetObj(form.acmeAccountAccessId);
const setting = JSON.parse(access.setting || "{}");
const account = parseAccount(setting.account);
if (!account?.accountUri) {
message.error("ACME账号授权缺少accountUri,请重新生成账号");
return;
}
const record = await api.BuildRecord({
domain: form.domain,
accountUri: account.accountUri,
wildcard: true,
persistUntil: form.persistUntil,
});
form.caType = account.caType;
form.accountUri = account.accountUri;
form.hostRecord = record.hostRecord;
form.recordValue = record.recordValue;
form.status = "pending";
}
async function verifyRecord(row: any) {
const ok = await api.Verify(row.id);
message[ok ? "success" : "error"](ok ? "校验成功" : "未找到匹配的TXT记录,请稍后重试");
await crudExpose.doRefresh();
return ok;
}
function showRecordHelp(row: any) {
openDnsPersistSettingDialog({
record: row,
async onDone() {
await crudExpose.doRefresh();
},
});
}
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
actionbar: {
buttons: {
add: {
icon: "ion:add-circle-outline",
},
},
},
rowHandle: {
minWidth: 120,
fixed: "right",
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: { width: 80, order: -999 },
form: { show: false },
},
domain: {
title: "域名",
type: "text",
search: { show: true },
form: {
required: true,
valueChange({ form }) {
fillRecord(form);
},
},
},
mainDomain: {
title: "主域名",
type: "text",
form: {
show: false,
},
column: {
width: 160,
order: 901,
},
},
wildcard: {
title: "通配符",
type: "dict-switch",
form: {
show: false,
value: true,
},
column: { show: false },
},
acmeAccountAccessId: {
title: "ACME账号授权",
type: "dict-select",
dict: accessDict,
form: {
required: true,
order: -9,
component: {
name: "AccessSelector",
vModel: "modelValue",
type: "acmeAccount",
subtype: compute(({ form }) => {
return form.caType;
}),
},
valueChange({ form }) {
fillRecord(form);
},
},
column: {
width: 180,
},
},
caType: {
title: "颁发机构",
type: "dict-select",
dict: Dicts.sslProviderDict,
form: {
required: true,
value: "letsencrypt",
order: -10,
valueChange({ form }) {
form.acmeAccountAccessId = null;
fillRecord(form);
},
},
column: { width: 120 },
},
persistUntil: {
title: "有效期至",
type: "datetime",
form: {
helper: "可选;为空表示长期有效",
order: 20,
valueChange({ form }) {
fillRecord(form);
},
},
column: { width: 180, order: 900 },
},
hostRecord: {
title: "TXT主机名",
type: "copyable",
form: {
show: false,
},
column: {
width: 220,
cellRender({ value }) {
return (
<a-tooltip title={value}>
<fs-copyable modelValue={value}></fs-copyable>
</a-tooltip>
);
},
},
},
recordValue: {
title: "请设置TXT记录",
type: "copyable",
form: {
show: false,
},
column: {
width: 380,
cellRender({ value }) {
return (
<a-tooltip title={value}>
<fs-copyable modelValue={value}></fs-copyable>
</a-tooltip>
);
},
},
},
dnsProviderType: {
title: "DNS服务商",
type: "dict-select",
dict: dnsProviderTypeDict,
form: {
show: false,
component: {
name: "DnsProviderSelector",
},
},
column: { show: false },
},
dnsProviderAccess: {
title: "DNS授权",
type: "dict-select",
dict: accessDict,
form: {
show: false,
component: {
name: "AccessSelector",
vModel: "modelValue",
type: compute(({ form }) => {
const type = form.dnsProviderType || "aliyun";
return dnsProviderTypeDict?.dataMap[type]?.accessType || type;
}),
},
},
column: { show: false },
},
status: {
title: "状态",
type: "dict-select",
dict: statusDict,
form: {
show: false,
value: "pending",
},
column: {
width: 120,
cellRender({ value, row }) {
async function resetStatus() {
Modal.confirm({
title: "重新校验",
content: "确认将该记录状态重置为待设置,并重新校验吗?",
onOk: async () => {
await api.UpdateObj({ id: row.id, status: "pending" });
await verifyRecord(row);
},
});
}
return (
<div class={"flex flex-left"}>
<fs-values-format modelValue={value} dict={statusDict}></fs-values-format>
{row.status === "valid" && (
<a-tooltip title="撤销并重新校验">
<fs-icon class={"ml-5 pointer color-yellow"} icon="solar:undo-left-square-bold" onClick={resetStatus}></fs-icon>
</a-tooltip>
)}
</div>
);
},
},
},
triggerValidate: {
title: "校验",
type: "text",
form: {
show: false,
},
column: {
conditionalRenderDisabled: true,
width: 210,
align: "center",
cellRender({ row }) {
return (
<a-space>
{row.status === "valid" ? (
<span class="text-gray-500">TXT记录</span>
) : (
<>
<a-button type="primary" size="small" onClick={() => showRecordHelp(row)}>
TXT
</a-button>
<a-button type="primary" size="small" onClick={() => verifyRecord(row)}>
</a-button>
</>
)}
</a-space>
);
},
},
},
accountUri: {
title: "Account URI",
type: "text",
form: { show: false },
column: { show: false },
},
},
},
};
}
@@ -0,0 +1,33 @@
<template>
<fs-page class="page-cert-dns-persist">
<template #header>
<div>
<div class="title">DNS持久验证记录</div>
<div class="text-orange-500 mt-5">当前仅 Let's Encrypt 测试环境可以申请 DNS 持久验证证书</div>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"></fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
defineOptions({
name: "DnsPersistRecord",
});
const context: any = {
permission: { isProjectPermission: true },
};
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
@@ -0,0 +1,133 @@
import { message } from "ant-design-vue";
import { reactive } from "vue";
import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import DnsProviderSelector from "/@/components/plugins/cert/dns-provider-selector/index.vue";
import { useFormDialog } from "/@/use/use-dialog";
import { CreateTxt, TriggerVerify } from "./api";
export type DnsPersistSettingRecord = {
id?: number;
mainDomain?: string;
hostRecord?: string;
recordValue?: string;
dnsProviderType?: string;
dnsProviderAccess?: number;
};
export function useDnsPersistSettingDialog() {
const { openFormDialog } = useFormDialog();
function copyableRow(label: string, value?: string) {
return (
<div class="mb-10 flex items-center">
<div style={{ width: "90px", flexShrink: 0 }}>{label}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<fs-copyable class="w-full" model-value={value || ""}></fs-copyable>
</div>
</div>
);
}
async function openDnsPersistSettingDialog(req: { record: DnsPersistSettingRecord; onDone?: () => Promise<void> | void }) {
const record = req.record;
const form = reactive({
mode: "manual",
dnsProviderType: record.dnsProviderType || "",
dnsProviderAccessType: "",
dnsProviderAccess: record.dnsProviderAccess || null,
});
async function submit() {
if (!record.id) {
return;
}
if (form.mode === "manual") {
await TriggerVerify(record.id);
message.success("已提交校验");
await req.onDone?.();
return;
}
if (!form.dnsProviderType || !form.dnsProviderAccess) {
throw new Error("请选择DNS服务商和授权");
}
await CreateTxt({
id: record.id,
dnsProviderType: form.dnsProviderType,
dnsProviderAccess: form.dnsProviderAccess,
});
message.success("TXT记录已创建");
await req.onDone?.();
}
await openFormDialog({
title: "设置DNS TXT记录",
wrapper: {
width: 680,
buttons: {
reset: {
show: false,
},
ok: {
show: true,
text: "确定",
},
},
},
body: () => (
<div>
<a-radio-group value={form.mode} buttonStyle="solid" class="mb-10" onUpdate:value={(value: string) => (form.mode = value)}>
<a-radio-button value="manual"></a-radio-button>
<a-radio-button value="auto"></a-radio-button>
</a-radio-group>
{form.mode === "manual" ? (
<div>
<a-alert class="mb-10" type="info" show-icon message="请到DNS解析控制台添加以下TXT记录,添加后点击确定会立即校验。" />
{copyableRow("主域名", record.mainDomain)}
{copyableRow("TXT主机名", record.hostRecord)}
{copyableRow("TXT值", record.recordValue)}
</div>
) : (
<div>
<a-alert class="mb-10" type="info" show-icon message="请选择DNS服务商和授权,系统会创建TXT记录,后续校验由后台完成。" />
{copyableRow("主域名", record.mainDomain)}
<div class="mb-10 flex items-center">
<div style={{ width: "90px", flexShrink: 0 }}>DNS服务商</div>
<div style={{ flex: 1, minWidth: 0 }}>
<DnsProviderSelector
class="w-full"
style={{ width: "100%" }}
modelValue={form.dnsProviderType}
onUpdate:modelValue={(value: string) => {
form.dnsProviderType = value;
form.dnsProviderAccess = null;
}}
onSelectedChange={(option: any) => {
form.dnsProviderAccessType = option?.accessType || form.dnsProviderType;
}}
/>
</div>
</div>
<div class="mb-10 flex items-center">
<div style={{ width: "90px", flexShrink: 0 }}>DNS授权</div>
<div style={{ flex: 1, minWidth: 0 }}>
<AccessSelector
modelValue={form.dnsProviderAccess}
type={form.dnsProviderAccessType || form.dnsProviderType || "aliyun"}
onUpdate:modelValue={(value: number) => {
form.dnsProviderAccess = value;
}}
/>
</div>
</div>
</div>
)}
</div>
),
onSubmit: submit,
});
}
return {
openDnsPersistSettingDialog,
};
}
@@ -92,6 +92,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
if (form.challengeType === "cname") {
throw new Error(t("certd.domain.cnameManagedInCnamePage"));
}
if (form.challengeType === "dns-persist") {
throw new Error("DNS持久验证记录请在DNS持久验证记录页面管理");
}
if (form.challengeType === "dns") {
const isSubdomain = await api.IsSubdomain({ domain: form.domain });
if (isSubdomain && !subdomainConfirmed.value) {
@@ -221,6 +224,17 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
crudExpose.getFormWrapperRef().close();
},
});
} else if (value === "dns-persist") {
Modal.confirm({
title: "请前往DNS持久验证记录页面添加记录",
content: "DNS持久验证需要先配置ACME账号和_validation-persist持久TXT记录,续期时不再增删DNS记录;当前仅 Let's Encrypt 测试环境可以申请。",
async onOk() {
router.push({
path: "/certd/cert/dns-persist",
});
crudExpose.getFormWrapperRef().close();
},
});
}
},
},
@@ -213,6 +213,10 @@ function useStepForm() {
const stepOpen = (step: any, emit: any) => {
callback.value = emit;
currentStep.value = merge({ input: {}, strategy: {} }, step);
// version 1
if (mode.value === "edit" && currentStep.value.type === "CertApply" && currentStep.value.input?.version == null) {
currentStep.value.input.version = 1;
}
if (step.type) {
changeCurrentPlugin(currentStep.value);
}
@@ -204,6 +204,7 @@ const hasNewVersion = computed(() => {
return isNewVersion(version.value, latestVersion.value);
});
async function loadLatestVersion() {
// version.value = settingsStore.app.version; //
latestVersion.value = await api.GetLatestVersion();
console.log("latestVersion", latestVersion.value);
@@ -37,7 +37,7 @@
</div>
<div class="flex-between mt-5">
<div class="flex-o"><fs-icon icon="ant-design:check-outlined" class="color-green mr-5" /> 监控站点数</div>
<suite-value :model-value="detail.monitorCount.max" :used="detail.monitorCount.used" unit="" />
<suite-value :model-value="detail.monitorCount.max" :used="detail.monitorCount.used" unit="" />
</div>
</div>
</template>
@@ -265,7 +265,6 @@ onMounted(() => {});
.input-right {
width: 160px;
margin-left: 10px;
background: #cfcfcf !important;
}
.forge-password {
@@ -7,7 +7,7 @@
<passkey-login></passkey-login>
<template v-for="item in oauthProviderList" :key="buildProviderKey(item)">
<div v-if="item.addonId" class="oauth-icon-button pointer" @click="goOauthLogin(item)">
<div><fs-icon :icon="item.icon" class="text-blue-600 text-40" /></div>
<div><fs-icon :icon="item.icon" class="text-40" /></div>
<div class="ellipsis title" :title="item.addonTitle || item.title">{{ item.addonTitle || item.title }}</div>
</div>
</template>
@@ -125,7 +125,6 @@ async function goOauthLogin(item: OauthProviderItem) {
}
.fs-icon {
font-size: 36px;
color: #006be6;
margin: 0px !important;
}
}
@@ -6,7 +6,7 @@
<div class="sys-plugin-config settings-form">
<a-form :model="formState" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form-item label="公共Google EAB授权" :name="['CertApply', 'sysSetting', 'input', 'googleCommonEabAccessId']">
<a-form-item v-show="false" label="公共Google EAB授权" :name="['CertApply', 'sysSetting', 'input', 'googleCommonEabAccessId']">
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.googleCommonEabAccessId" type="eab" from="sys"></access-selector>
<div class="helper">
<div>设置公共Google EAB授权给用户使用避免用户自己去翻墙获取Google EAB授权</div>
@@ -16,7 +16,14 @@
</div>
</a-form-item>
<a-form-item label="公共ZeroSSL EAB授权" :name="['CertApply', 'sysSetting', 'input', 'zerosslCommonEabAccessId']">
<a-form-item label="公共Google ACME账号" :name="['CertApply', 'sysSetting', 'input', 'googleCommonAcmeAccountAccessId']">
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.googleCommonAcmeAccountAccessId" type="acmeAccount" subtype="google" from="sys"></access-selector>
<div class="helper">
<div>优先推荐配置公共ACME账号配置后普通用户申请Google证书时无需选择账号也不会重复消费公共EAB</div>
</div>
</a-form-item>
<a-form-item v-show="false" label="公共ZeroSSL EAB授权" :name="['CertApply', 'sysSetting', 'input', 'zerosslCommonEabAccessId']">
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.zerosslCommonEabAccessId" type="eab" from="sys"></access-selector>
<div class="helper">
<div>设置公共ZeroSSL EAB授权给用户使用避免用户自己去翻墙获取Zero EAB授权</div>
@@ -26,7 +33,11 @@
</div>
</a-form-item>
<a-form-item label="公共litessl EAB授权" :name="['CertApply', 'sysSetting', 'input', 'litesslCommonEabAccessId']">
<a-form-item label="公共ZeroSSL ACME账号" :name="['CertApply', 'sysSetting', 'input', 'zerosslCommonAcmeAccountAccessId']">
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.zerosslCommonAcmeAccountAccessId" type="acmeAccount" subtype="zerossl" from="sys"></access-selector>
</a-form-item>
<a-form-item v-show="false" label="公共litessl EAB授权" :name="['CertApply', 'sysSetting', 'input', 'litesslCommonEabAccessId']">
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.litesslCommonEabAccessId" type="eab" from="sys"></access-selector>
<div class="helper">
<div>设置公共litessl EAB授权给用户使用避免用户自己获取litessl EAB授权</div>
@@ -36,6 +47,10 @@
</div>
</a-form-item>
<a-form-item label="公共litessl ACME账号" :name="['CertApply', 'sysSetting', 'input', 'litesslCommonAcmeAccountAccessId']">
<access-selector v-model:model-value="formState.CertApply.sysSetting.input.litesslCommonAcmeAccountAccessId" type="acmeAccount" subtype="litessl" from="sys"></access-selector>
</a-form-item>
<a-form-item label="其他配置">
<a-button type="primary" @click="doPluginConfig">证书申请插件默认值设置</a-button>
<div class="helper">
@@ -1,5 +1,5 @@
<template>
<a-tag color="green"> {{ durationDict.dataMap[modelValue]?.label }}</a-tag>
<a-tag color="green"> {{ durationDict.dataMap[modelValue]?.label || modelValue + "天" }}</a-tag>
</template>
<script lang="ts" setup>
@@ -123,10 +123,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
value: "id",
label: "nickName",
}),
editForm: {
component: {
disabled: true,
},
},
form: {
show: true,
component: {
disabled: true,
disabled: false,
crossPage: true,
multiple: false,
select: {
@@ -170,11 +175,6 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
productType: {
title: t("certd.type"),
type: "dict-select",
editForm: {
component: {
disabled: true,
},
},
dict: dict({
data: [
{ label: t("certd.package"), value: "suite", color: "green" },
@@ -182,7 +182,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
],
}),
form: {
show: true,
show: false,
component: {
disabled: true,
},
@@ -205,6 +205,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
"content.maxDomainCount": {
title: t("certd.domain_count"),
type: "text",
addForm: {
show: false,
},
form: {
key: ["content", "maxDomainCount"],
component: {
@@ -227,6 +230,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
"content.maxWildcardDomainCount": {
title: t("certd.wildcardDomainCountPart"),
type: "text",
addForm: {
show: false,
},
form: {
key: ["content", "maxWildcardDomainCount"],
component: {
@@ -249,6 +255,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
"content.maxPipelineCount": {
title: t("certd.pipeline_count"),
type: "text",
addForm: {
show: false,
},
form: {
key: ["content", "maxPipelineCount"],
component: {
@@ -271,6 +280,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
"content.maxDeployCount": {
title: t("certd.deploy_count"),
type: "text",
addForm: {
show: false,
},
form: {
key: ["content", "maxDeployCount"],
component: {
@@ -296,6 +308,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
"content.maxMonitorCount": {
title: t("certd.monitor_count"),
type: "text",
addForm: {
show: false,
},
form: {
key: ["content", "maxMonitorCount"],
component: {
@@ -317,7 +332,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
duration: {
title: t("certd.duration"),
type: "text",
type: "number",
addForm: {
show: false,
},
form: {
rules: [{ required: true, message: t("certd.field_required") }],
},
@@ -363,6 +381,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
expiresTime: {
title: t("certd.expires_time"),
type: "date",
addForm: {
show: false,
},
form: {
valueBuilder: ({ value }) => {
return dayjs(value);
@@ -393,6 +414,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
{ label: t("certd.is_present_no"), value: false, color: "blue" },
],
}),
addForm: {
show: false,
},
form: {
value: true,
},
@@ -404,6 +428,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
deployCountUsed: {
title: t("certd.deploy_count_used"),
type: "number",
addForm: {
show: false,
},
form: {
value: 0,
rules: [{ required: true, message: t("certd.field_required") }],
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.40.3](https://github.com/certd/certd/compare/v1.40.2...v1.40.3) (2026-05-21)
### Bug Fixes
* 修复暗黑模式下注册页面验证码看不清的问题 ([5ba33be](https://github.com/certd/certd/commit/5ba33be30f765f06cafbfcc04f5e25320db01449))
## [1.40.2](https://github.com/certd/certd/compare/v1.40.1...v1.40.2) (2026-05-19)
### Bug Fixes
* **certd-server:** 调整首页缓存控制头的判断逻辑 ([0499347](https://github.com/certd/certd/commit/0499347588ee544862420ab9a5afd2546d61bc6c))
### Performance Improvements
* **controller:** 更换版本获取源并添加版本标准化处理 ([cb08e06](https://github.com/certd/certd/commit/cb08e061d257ba23a0fefdbfb046a8c759def828))
## [1.40.1](https://github.com/certd/certd/compare/v1.40.0...v1.40.1) (2026-05-18)
### Bug Fixes
@@ -0,0 +1,31 @@
ALTER TABLE cd_access ADD COLUMN subtype varchar(100);
CREATE INDEX "index_access_subtype" ON "cd_access" ("subtype");
CREATE TABLE "cd_dns_persist_record"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer NOT NULL,
"project_id" integer,
"domain" varchar(255) NOT NULL,
"main_domain" varchar(255) NOT NULL,
"ca_type" varchar(50) NOT NULL,
"acme_account_access_id" integer NOT NULL,
"account_uri" varchar(512) NOT NULL,
"host_record" varchar(255) NOT NULL,
"record_value" text NOT NULL,
"policy" varchar(50),
"persist_until" integer,
"status" varchar(50) NOT NULL DEFAULT 'pending',
"dns_provider_type" varchar(50),
"dns_provider_access" integer,
"record_res" text,
"disabled" integer NOT NULL DEFAULT 0,
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_dns_persist_user_id" ON "cd_dns_persist_record" ("user_id");
CREATE INDEX "index_dns_persist_project_id" ON "cd_dns_persist_record" ("project_id");
CREATE INDEX "index_dns_persist_domain" ON "cd_dns_persist_record" ("domain");
CREATE INDEX "index_dns_persist_main_domain" ON "cd_dns_persist_record" ("main_domain");
CREATE INDEX "index_dns_persist_account" ON "cd_dns_persist_record" ("acme_account_access_id");
@@ -21,10 +21,10 @@ input:
options:
- label: QQ
value: qq
icon: cib:tencent-qq:#007AFF
icon: svg:icon-qq
- label: 微信
value: wx
icon: simple-icons:wechat:#34C759
icon: svg:icon-wechat
- label: 支付宝
value: alipay
icon: simple-icons:alipay:#0099ff
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.40.1",
"version": "1.40.3",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -53,20 +53,20 @@
"@aws-sdk/client-sts": "^3.990.0",
"@azure/arm-dns": "^5.1.0",
"@azure/identity": "^4.13.1",
"@certd/acme-client": "^1.40.1",
"@certd/basic": "^1.40.1",
"@certd/commercial-core": "^1.40.1",
"@certd/acme-client": "^1.40.3",
"@certd/basic": "^1.40.3",
"@certd/commercial-core": "^1.40.3",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.40.1",
"@certd/lib-huawei": "^1.40.1",
"@certd/lib-k8s": "^1.40.1",
"@certd/lib-server": "^1.40.1",
"@certd/midway-flyway-js": "^1.40.1",
"@certd/pipeline": "^1.40.1",
"@certd/plugin-cert": "^1.40.1",
"@certd/plugin-lib": "^1.40.1",
"@certd/plugin-plus": "^1.40.1",
"@certd/plus-core": "^1.40.1",
"@certd/jdcloud": "^1.40.3",
"@certd/lib-huawei": "^1.40.3",
"@certd/lib-k8s": "^1.40.3",
"@certd/lib-server": "^1.40.3",
"@certd/midway-flyway-js": "^1.40.3",
"@certd/pipeline": "^1.40.3",
"@certd/plugin-cert": "^1.40.3",
"@certd/plugin-lib": "^1.40.3",
"@certd/plugin-plus": "^1.40.3",
"@certd/plus-core": "^1.40.3",
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
@@ -1,6 +1,11 @@
export function shouldSetDefaultNoCache(path: string, cacheControl?: string) {
if(path === '/' || path === '/index.html' ){
//首页不管怎样都不要缓存
return true;
}
if (cacheControl) {
return false;
}
return path === '/' || path === '/index.html' || path.startsWith('/api');
// api也不要缓存,如果他本身有设置缓存除外
return path.startsWith('/api');
}
@@ -13,7 +13,9 @@ describe("shouldSetDefaultNoCache", () => {
});
it("keeps explicit cache headers from file responses", () => {
assert.equal(shouldSetDefaultNoCache("/api/basic/file/download", "public,max-age=259200"), false);
assert.equal(shouldSetDefaultNoCache("/", "public,max-age=259200"), true);
assert.equal(shouldSetDefaultNoCache("/index.html", "public,max-age=259200"), true);
assert.equal(shouldSetDefaultNoCache("/api/basic/file/download", "public,max-age=259200"), false);
});
it("ignores non-html and non-api paths", () => {
@@ -126,7 +126,7 @@ export class MainConfiguration {
await next();
const path = ctx.path;
// 如果是首页则强制设置为不缓存
if (path === '/' || path === '/index.html' || shouldSetDefaultNoCache(path, ctx.response.get('Cache-Control')) ) {
if (shouldSetDefaultNoCache(path, ctx.response.get('Cache-Control')) ) {
ctx.response.set('Cache-Control', 'public,max-age=0');
}
});
@@ -138,6 +138,5 @@ export class MainConfiguration {
logger.info('当前环境:', this.app.getEnv()); // prod
// throw new Error("address family not supported")
}
}
@@ -0,0 +1,91 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
import { Constants, CrudController } from "@certd/lib-server";
import { ApiTags } from "@midwayjs/swagger";
import { DnsPersistRecordService } from "../../../modules/cert/service/dns-persist-record-service.js";
@Provide()
@Controller("/api/cert/dns-persist")
@ApiTags(["cert"])
export class DnsPersistRecordController extends CrudController<DnsPersistRecordService> {
@Inject()
service: DnsPersistRecordService;
getService(): DnsPersistRecordService {
return this.service;
}
@Post("/page", { description: Constants.per.authOnly, summary: "查询DNS持久验证记录分页列表" })
async page(@Body(ALL) body: any) {
const { projectId, userId } = await this.getProjectUserIdRead();
body.query = body.query ?? {};
body.query.projectId = projectId;
body.query.userId = userId;
return super.page(body);
}
@Post("/add", { description: Constants.per.authOnly, summary: "添加DNS持久验证记录" })
async add(@Body(ALL) bean: any) {
const { projectId, userId } = await this.getProjectUserIdWrite();
bean.projectId = projectId;
bean.userId = userId;
return super.add(bean);
}
@Post("/update", { description: Constants.per.authOnly, summary: "更新DNS持久验证记录" })
async update(@Body(ALL) bean: any) {
await this.checkOwner(this.getService(), bean.id, "write");
delete bean.userId;
delete bean.projectId;
return super.update(bean);
}
@Post("/info", { description: Constants.per.authOnly, summary: "查询DNS持久验证记录详情" })
async info(@Query("id") id: number) {
await this.checkOwner(this.getService(), id, "read");
return super.info(id);
}
@Post("/delete", { description: Constants.per.authOnly, summary: "删除DNS持久验证记录" })
async delete(@Query("id") id: number) {
await this.checkOwner(this.getService(), id, "write");
await this.service.delete(id as any);
return this.ok({
message: this.service.lastDeleteMessage,
});
}
@Post("/build", { description: Constants.per.authOnly, summary: "生成DNS持久验证记录值" })
async build(@Body(ALL) body: { domain: string; accountUri: string; wildcard?: boolean; persistUntil?: number }) {
const { projectId, userId } = await this.getProjectUserIdRead();
return this.ok(await this.service.buildRecord({ ...body, userId, projectId }));
}
@Post("/getByDomain", { description: Constants.per.authOnly, summary: "根据域名获取或创建DNS持久验证记录" })
async getByDomain(@Body(ALL) body: { domain: string; caType?: string; acmeAccountAccessId?: number; commonAcmeAccountAccessId?: number; wildcard?: boolean; persistUntil?: number; createOnNotFound?: boolean }) {
const { projectId, userId } = await this.getProjectUserIdWrite();
return this.ok(await this.service.getByDomain({ ...body, userId, projectId }));
}
@Post("/check", { description: Constants.per.authOnly, summary: "校验DNS持久验证记录" })
async check(@Body(ALL) body: { hostRecord: string; recordValue: string }) {
return this.ok(await this.service.checkRecord(body));
}
@Post("/verify", { description: Constants.per.authOnly, summary: "验证DNS持久验证记录" })
async verify(@Body(ALL) body: { id: number }) {
await this.checkOwner(this.getService(), body.id, "write");
return this.ok(await this.service.verify(body.id));
}
@Post("/triggerVerify", { description: Constants.per.authOnly, summary: "后台验证DNS持久验证记录" })
async triggerVerify(@Body(ALL) body: { id: number }) {
await this.checkOwner(this.getService(), body.id, "write");
return this.ok(await this.service.triggerVerify(body.id));
}
@Post("/createTxt", { description: Constants.per.authOnly, summary: "一键创建DNS持久验证TXT记录" })
async createTxt(@Body(ALL) body: { id: number; dnsProviderType?: string; dnsProviderAccess?: number }) {
await this.checkOwner(this.getService(), body.id, "write");
return this.ok(await this.service.createDnsTxt(body));
}
}
@@ -23,31 +23,49 @@ describe("AutoFix", () => {
autoFix.googleCommonEabAccountKeyFix = {
async init() {
calls.push("google");
return true;
},
} as any;
autoFix.oauthSubtypeBoundTypeFix = {
async init() {
calls.push("oauth");
return true;
},
} as any;
autoFix.certInfoWildcardDomainCountFix = {
async init() {
calls.push("cert");
return true;
},
} as any;
autoFix.suiteContentWildcardDomainCountFix = {
async init() {
calls.push("suite");
return true;
},
} as any;
autoFix.legacyAcmeAccountAccessFix = {
async init() {
calls.push("legacy-acme");
return true;
},
} as any;
autoFix.commonEabToAcmeAccountFix = {
async init() {
calls.push("common-eab-acme");
return true;
},
} as any;
await autoFix.init();
assert.deepEqual(calls, ["google", "cert", "suite"]);
assert.deepEqual(calls, ["google", "cert", "suite", "legacy-acme", "common-eab-acme"]);
assert.equal(savedSetting.fixed["google-common-eab-account-key"], true);
assert.equal(savedSetting.fixed["oauth-subtype-bound-type"], true);
assert.equal(savedSetting.fixed["cert-info-wildcard-domain-count"], true);
assert.equal(savedSetting.fixed["suite-content-wildcard-domain-count"], true);
assert.equal(savedSetting.fixed["legacy-acme-account-access"], true);
assert.equal(savedSetting.fixed["common-eab-to-acme-account"], true);
});
it("initializes missing fixed map", async () => {
@@ -62,6 +80,8 @@ describe("AutoFix", () => {
autoFix.oauthSubtypeBoundTypeFix = { async init() {} } as any;
autoFix.certInfoWildcardDomainCountFix = { async init() {} } as any;
autoFix.suiteContentWildcardDomainCountFix = { async init() {} } as any;
autoFix.legacyAcmeAccountAccessFix = { async init() {} } as any;
autoFix.commonEabToAcmeAccountFix = { async init() {} } as any;
await autoFix.init();
});
@@ -4,11 +4,13 @@ import { GoogleCommonEabAccountKeyFix } from "./google-common-eab-account-key-fi
import { OauthSubtypeBoundTypeFix } from "./oauth-subtype-bound-type-fix.js";
import { CertInfoWildcardDomainCountFix } from "./cert-info-wildcard-domain-count-fix.js";
import { SuiteContentWildcardDomainCountFix } from "./suite-content-wildcard-domain-count-fix.js";
import { LegacyAcmeAccountAccessFix } from "./legacy-acme-account-access-fix.js";
import { CommonEabToAcmeAccountFix } from "./common-eab-to-acme-account-fix.js";
type AutoFixTask = {
key: string;
fix: {
init(): Promise<void>;
init(): Promise<boolean>;
};
};
@@ -30,6 +32,12 @@ export class AutoFix {
@Inject()
suiteContentWildcardDomainCountFix: SuiteContentWildcardDomainCountFix;
@Inject()
legacyAcmeAccountAccessFix: LegacyAcmeAccountAccessFix;
@Inject()
commonEabToAcmeAccountFix: CommonEabToAcmeAccountFix;
async init() {
const setting = await this.sysSettingsService.getSetting<SysAutoFixSetting>(SysAutoFixSetting);
setting.fixed = setting.fixed || {};
@@ -50,14 +58,22 @@ export class AutoFix {
key: "suite-content-wildcard-domain-count",
fix: this.suiteContentWildcardDomainCountFix,
},
{
key: "legacy-acme-account-access",
fix: this.legacyAcmeAccountAccessFix,
},
{
key: "common-eab-to-acme-account",
fix: this.commonEabToAcmeAccountFix,
},
];
for (const task of tasks) {
if (setting.fixed?.[task.key]) {
continue;
}
await task.fix.init();
setting.fixed[task.key] = true;
const ret = await task.fix.init();
setting.fixed[task.key] = ret;
await this.sysSettingsService.saveSetting(setting);
}
}
@@ -38,8 +38,10 @@ export class CertInfoWildcardDomainCountFix {
if (fixedCount > 0) {
logger.info(`已修复证书泛域名数量历史数据,数量=${fixedCount}`);
}
return true
} catch (e: any) {
logger.error("修复证书泛域名数量历史数据失败", e);
}
return false
}
}
@@ -0,0 +1,135 @@
import assert from "assert";
import { buildLegacyCommonEabAccountStorageWhere, CommonEabToAcmeAccountFix, parseEabAccountKey } from "./common-eab-to-acme-account-fix.js";
import { AcmeService } from "../../../plugins/plugin-cert/plugin/cert-plugin/acme.js";
describe("CommonEabToAcmeAccountFix", () => {
it("parses legacy EAB account key payload", () => {
assert.equal(
parseEabAccountKey(
JSON.stringify({
kid: "kid-1",
privateKey: "private-key",
})
),
"private-key"
);
});
it("builds legacy common EAB account storage query", () => {
assert.deepEqual(buildLegacyCommonEabAccountStorageWhere("google", 12), {
userId: 0,
scope: "user",
namespace: "0",
key: "acme.config.google.access.12",
});
});
it("creates common acme account from common eab and legacy storage", async () => {
let addParam: any;
const fix = new CommonEabToAcmeAccountFix();
fix.accessService = {
async getAccessById(id: number) {
assert.equal(id, 12);
return {
accountKey: JSON.stringify({
privateKey: "private-key",
}),
email: "common@example.com",
};
},
async findOne() {
return null;
},
async add(param: any) {
addParam = param;
return { id: 99 };
},
} as any;
fix.storageService = {
getRepository() {
return {
async findOne(options: any) {
assert.deepEqual(options.where, buildLegacyCommonEabAccountStorageWhere("google", 12));
return {
value: JSON.stringify({
value: {
accountUrl: "https://example.com/acct/1",
},
}),
};
},
};
},
} as any;
const id = await fix.createCommonAcmeAccountFromEab("google", 12);
assert.equal(id, 99);
assert.equal(addParam.userId, 0);
assert.equal(addParam.type, "acmeAccount");
const setting = JSON.parse(addParam.setting);
const account = JSON.parse(setting.account);
assert.equal(account.accountKey, "private-key");
assert.equal(account.accountUri, "https://example.com/acct/1");
});
it("creates common acme account by resolving account uri from eab private key", async () => {
const original = AcmeService.prototype.getAcmeClient;
const calls: string[] = [];
AcmeService.prototype.getAcmeClient = async function (email: string) {
calls.push(email);
return {
getAccountUrl() {
return "https://example.com/acct/generated";
},
} as any;
};
try {
let addParam: any;
const fix = new CommonEabToAcmeAccountFix();
fix.accessService = {
async getAccessById(id: number) {
assert.equal(id, 12);
return {
id: 12,
kid: "kid-1",
hmacKey: "hmac-1",
accountKey: JSON.stringify({
kid: "kid-1",
privateKey: "private-key",
}),
email: "common@example.com",
};
},
async findOne() {
return null;
},
async add(param: any) {
addParam = param;
return { id: 100 };
},
} as any;
fix.storageService = {
getRepository() {
return {
async findOne() {
return null;
},
};
},
} as any;
const id = await fix.createCommonAcmeAccountFromEab("google", 12);
assert.equal(id, 100);
assert.deepEqual(calls, ["common@example.com"]);
const setting = JSON.parse(addParam.setting);
const account = JSON.parse(setting.account);
assert.equal(account.accountKey, "private-key");
assert.equal(account.accountUri, "https://example.com/acct/generated");
} finally {
AcmeService.prototype.getAcmeClient = original;
}
});
});
@@ -0,0 +1,188 @@
import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { PluginConfigService } from "../../plugin/service/plugin-config-service.js";
import { StorageService } from "../../pipeline/service/storage-service.js";
import { AcmeService } from "../../../plugins/plugin-cert/plugin/cert-plugin/acme.js";
import { buildAcmeAccountSetting, LegacyAcmeAccountConfig } from "./legacy-acme-account-access-fix.js";
import { parseStorageValue } from "./google-common-eab-account-key-fix.js";
const COMMON_EAB_TO_ACME_ACCOUNT_FIELDS = [
{
caType: "google",
eabField: "googleCommonEabAccessId",
acmeField: "googleCommonAcmeAccountAccessId",
},
{
caType: "zerossl",
eabField: "zerosslCommonEabAccessId",
acmeField: "zerosslCommonAcmeAccountAccessId",
},
{
caType: "sslcom",
eabField: "sslcomCommonEabAccessId",
acmeField: "sslcomCommonAcmeAccountAccessId",
},
{
caType: "litessl",
eabField: "litesslCommonEabAccessId",
acmeField: "litesslCommonAcmeAccountAccessId",
},
];
export function parseEabAccountKey(accountKey?: string) {
if (!accountKey) {
return null;
}
try {
const parsed = JSON.parse(accountKey);
return parsed?.privateKey || parsed?.accountKey || parsed?.key || accountKey;
} catch {
return accountKey;
}
}
export function buildLegacyCommonEabAccountStorageWhere(caType: string, accessId: number) {
return {
userId: 0,
scope: "user",
namespace: "0",
key: `acme.config.${caType}.access.${accessId}`,
};
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CommonEabToAcmeAccountFix {
@Inject()
pluginConfigService: PluginConfigService;
@Inject()
accessService: AccessService;
@Inject()
storageService: StorageService;
async init() {
try {
const certApplyConfig = await this.pluginConfigService.getPluginConfig({
name: "CertApply",
type: "builtIn",
});
const input = certApplyConfig.sysSetting.input || {};
let changed = false;
for (const item of COMMON_EAB_TO_ACME_ACCOUNT_FIELDS) {
if (input[item.acmeField]) {
continue;
}
const eabAccessId = input[item.eabField];
if (!eabAccessId) {
continue;
}
const acmeAccessId = await this.createCommonAcmeAccountFromEab(item.caType, eabAccessId);
if (acmeAccessId) {
input[item.acmeField] = acmeAccessId;
changed = true;
}
}
if (changed) {
await this.pluginConfigService.savePluginConfig({
name: "CertApply",
disabled: certApplyConfig.disabled,
sysSetting: {
...certApplyConfig.sysSetting,
input,
},
});
}
return true;
} catch (e: any) {
logger.error("公共EAB迁移为公共ACME账号失败", e);
return false;
}
}
async createCommonAcmeAccountFromEab(caType: string, eabAccessId: number) {
const eabAccess = await this.accessService.getAccessById(eabAccessId, false);
const privateKey = parseEabAccountKey(eabAccess.accountKey);
const accountConfig = await this.getLegacyCommonEabAccountConfig(caType, eabAccessId);
const accountUri = await this.resolveAccountUriByPrivateKey(caType, eabAccess, accountConfig?.accountUri || accountConfig?.accountUrl);
if (!privateKey || !accountUri) {
logger.info(`公共${caType} EAB缺少可迁移的accountKey或无法获取accountUri,跳过生成公共ACME账号`);
return null;
}
const email = eabAccess.email || `${caType}@common.certd.local`;
const exists = await this.accessService.findOne({
where: {
userId: 0,
projectId: null,
type: "acmeAccount",
subtype: caType,
name: `公共${caType} ACME账号`,
} as any,
});
if (exists) {
return exists.id;
}
const setting = buildAcmeAccountSetting({
caType,
email,
config: {
privateKey,
accountUri,
},
});
const { id } = await this.accessService.add({
userId: 0,
projectId: null,
type: "acmeAccount",
name: `公共${caType} ACME账号`,
setting: JSON.stringify(setting),
});
logger.info(`已根据公共${caType} EAB生成公共ACME账号,accessId=${id}`);
return id;
}
async resolveAccountUriByPrivateKey(caType: string, eabAccess: any, accountUri?: string | null) {
if (accountUri) {
return accountUri;
}
const privateKey = parseEabAccountKey(eabAccess.accountKey);
if (!privateKey || !eabAccess?.kid) {
return null;
}
const acmeService = new AcmeService({
userId: 0,
userContext: {
async getObj() {
return null;
},
async setObj() {},
} as any,
logger: logger as any,
sslProvider: caType as any,
eab: {
id: eabAccess.id || eabAccess.accessId || eabAccess.eabAccessId || 0,
kid: eabAccess.kid,
hmacKey: eabAccess.hmacKey,
accountKey: JSON.stringify({
kid: eabAccess.kid,
privateKey,
}),
} as any,
domainParser: {} as any,
privateKeyType: "rsa_2048",
});
const client = await acmeService.getAcmeClient(eabAccess.email || `${caType}@common.certd.local`);
return client.getAccountUrl() || null;
}
async getLegacyCommonEabAccountConfig(caType: string, accessId: number): Promise<LegacyAcmeAccountConfig | null> {
const repository = this.storageService.getRepository();
const record = await repository.findOne({
where: buildLegacyCommonEabAccountStorageWhere(caType, accessId),
});
return parseStorageValue(record?.value) as LegacyAcmeAccountConfig;
}
}
@@ -47,7 +47,7 @@ export class GoogleCommonEabAccountKeyFix {
async init() {
if (!isComm()) {
return;
return true;
}
try {
const certApplyConfig = await this.pluginConfigService.getPluginConfig({
@@ -56,31 +56,33 @@ export class GoogleCommonEabAccountKeyFix {
});
const googleCommonEabAccessId = certApplyConfig?.sysSetting?.input?.googleCommonEabAccessId;
if (!googleCommonEabAccessId) {
return;
return true;
}
const eabAccess = await this.accessService.getAccessById(googleCommonEabAccessId, false);
if (eabAccess.accountKey) {
return;
return true;
}
if (!eabAccess.kid) {
logger.info("公共Google EAB授权缺少KID,跳过历史ACME账号私钥修复");
return;
return true;
}
const accountConfig = await this.getLegacyGoogleAccountConfig(eabAccess.email);
const privateKey = accountConfig?.privateKey || accountConfig?.key || accountConfig?.accountKey;
if (!privateKey) {
logger.info("未找到可迁移到公共Google EAB授权的历史ACME账号私钥");
return;
return true;
}
const accountKey = buildEabAccountKeyValue(eabAccess.kid, privateKey);
await this.accessService.updateAccess({ id: googleCommonEabAccessId, eabType: "google", accountKey });
logger.info(`已修复公共Google EAB授权的ACME账号私钥,accessId=${googleCommonEabAccessId}`);
return true;
} catch (e: any) {
logger.error("修复公共Google EAB授权ACME账号私钥失败", e);
}
return false
}
async getLegacyGoogleAccountConfig(email?: string) {
@@ -0,0 +1,48 @@
import assert from "assert";
import { buildAcmeAccountAccessName, buildAcmeAccountSetting, maskAcmeAccountEmail, parseLegacyAcmeStorageKey } from "./legacy-acme-account-access-fix.js";
describe("LegacyAcmeAccountAccessFix", () => {
it("parses legacy storage account key", () => {
assert.deepEqual(parseLegacyAcmeStorageKey("acme.config.letsencrypt.user@example.com"), {
caType: "letsencrypt",
email: "user@example.com",
});
});
it("skips EAB access cache keys", () => {
assert.equal(parseLegacyAcmeStorageKey("acme.config.google.access.12"), null);
});
it("builds acme account access setting from legacy config", () => {
const setting = buildAcmeAccountSetting({
caType: "letsencrypt",
email: "user@example.com",
config: {
key: "private-key",
accountUrl: "https://example.com/acct/1",
},
});
assert.equal(setting.caType, "letsencrypt");
const account = JSON.parse(setting.account);
assert.equal(account.accountKey, "private-key");
assert.equal(account.accountUri, "https://example.com/acct/1");
});
it("builds masked acme account access name", () => {
assert.equal(maskAcmeAccountEmail("xiaojunnuo@qq.com"), "xi*******qq.com");
assert.equal(buildAcmeAccountAccessName("zerossl", "xiaojunnuo@qq.com"), "zerossl-acme-xi*******qq.com");
});
it("skips incomplete legacy config", () => {
const setting = buildAcmeAccountSetting({
caType: "letsencrypt",
email: "user@example.com",
config: {
key: "private-key",
},
});
assert.equal(setting, null);
});
});
@@ -0,0 +1,131 @@
import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { Like } from "typeorm";
import { StorageService } from "../../pipeline/service/storage-service.js";
import { parseStorageValue } from "./google-common-eab-account-key-fix.js";
export type LegacyAcmeAccountConfig = {
key?: string;
privateKey?: string;
accountKey?: string;
accountUrl?: string;
accountUri?: string;
};
export function parseLegacyAcmeStorageKey(key: string) {
const match = /^acme\.config\.([^.]+)\.(.+)$/.exec(key);
if (!match) {
return null;
}
if (match[2].startsWith("access.")) {
return null;
}
return {
caType: match[1],
email: match[2],
};
}
export function buildAcmeAccountSetting(req: { caType: string; email: string; config: LegacyAcmeAccountConfig }) {
const accountKey = req.config.privateKey || req.config.key || req.config.accountKey;
const accountUri = req.config.accountUri || req.config.accountUrl;
if (!accountKey || !accountUri) {
return null;
}
return {
caType: req.caType,
email: req.email,
account: JSON.stringify({
accountKey,
accountUri,
caType: req.caType,
email: req.email,
directoryUrl: "",
migratedFrom: "legacy-storage",
}),
};
}
export function maskAcmeAccountEmail(email: string) {
if (!email) {
return "unknown";
}
const atIndex = email.indexOf("@");
if (atIndex < 0) {
return email.length <= 2 ? `${email[0] || ""}*******` : `${email.substring(0, 2)}*******`;
}
const name = email.substring(0, atIndex);
const domain = email.substring(atIndex + 1);
const prefix = name.substring(0, Math.min(2, name.length));
return `${prefix}*******${domain}`;
}
export function buildAcmeAccountAccessName(caType: string, email: string) {
return `${caType}-acme-${maskAcmeAccountEmail(email)}`;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class LegacyAcmeAccountAccessFix {
@Inject()
storageService: StorageService;
@Inject()
accessService: AccessService;
async init() {
try {
const repository = this.storageService.getRepository();
const records = await repository.find({
where: {
scope: "user",
key: Like("acme.config.%"),
},
});
let count = 0;
for (const record of records) {
const parsedKey = parseLegacyAcmeStorageKey(record.key);
if (!parsedKey) {
continue;
}
const config = parseStorageValue(record.value) as LegacyAcmeAccountConfig;
const setting = buildAcmeAccountSetting({
...parsedKey,
config,
});
if (!setting) {
continue;
}
const name = buildAcmeAccountAccessName(parsedKey.caType, parsedKey.email);
const exists = await this.accessService.findOne({
where: {
userId: record.userId,
projectId: record.projectId,
type: "acmeAccount",
subtype: parsedKey.caType,
name,
} as any,
});
if (exists) {
continue;
}
await this.accessService.add({
userId: record.userId,
projectId: record.projectId,
type: "acmeAccount",
subtype: parsedKey.caType,
name,
setting: JSON.stringify(setting),
});
count++;
}
logger.info(`旧ACME账号迁移完成,生成${count}个ACME账号授权`);
return true;
} catch (e: any) {
logger.error("旧ACME账号迁移失败", e);
return false;
}
}
}
@@ -41,6 +41,7 @@ export class OauthSubtypeBoundTypeFix {
await this.convertLegacyAddonLoginTypeToArray(addonEntity, legacyLoginType, manager);
}
});
return true
} catch (e: any) {
logger.error("修复OAuth subtype绑定历史数据失败", e);
}
@@ -33,9 +33,11 @@ export class SuiteContentWildcardDomainCountFix {
if (fixedCount > 0) {
logger.info(`已修复套餐最大泛域名数量历史数据,数量=${fixedCount}`);
}
return true
} catch (e: any) {
logger.error("修复套餐最大泛域名数量历史数据失败", e);
}
return false
}
private async fixSuiteContentWildcardDomainCountByTable(entityManager: any, tableName: string) {
@@ -0,0 +1,61 @@
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity("cd_dns_persist_record")
export class DnsPersistRecordEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: "user_id" })
userId: number;
@Column({ name: "project_id", nullable: true })
projectId: number;
@Column({ length: 255 })
domain: string;
@Column({ name: "main_domain", length: 255 })
mainDomain: string;
@Column({ name: "ca_type", length: 50 })
caType: string;
@Column({ name: "acme_account_access_id" })
acmeAccountAccessId: number;
@Column({ name: "account_uri", length: 512 })
accountUri: string;
@Column({ name: "host_record", length: 255 })
hostRecord: string;
@Column({ name: "record_value", type: "text" })
recordValue: string;
@Column({ length: 50, nullable: true })
policy: string;
@Column({ name: "persist_until", nullable: true })
persistUntil: number;
@Column({ length: 50 })
status: string;
@Column({ name: "dns_provider_type", length: 50, nullable: true })
dnsProviderType: string;
@Column({ name: "dns_provider_access", nullable: true })
dnsProviderAccess: number;
@Column({ name: "record_res", type: "text", nullable: true })
recordRes: string;
@Column()
disabled: boolean;
@Column({ name: "create_time", default: () => "CURRENT_TIMESTAMP" })
createTime: Date;
@Column({ name: "update_time", default: () => "CURRENT_TIMESTAMP" })
updateTime: Date;
}
@@ -0,0 +1,315 @@
import assert from "assert";
import { buildDnsPersistRecordValue, DnsPersistRecordService } from "./dns-persist-record-service.js";
describe("DnsPersistRecordService", () => {
it("builds dns-persist-01 record value", () => {
const value = buildDnsPersistRecordValue({
accountUri: "https://example.com/acct/1",
wildcard: true,
persistUntil: 1893456000,
});
assert.equal(value, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard; persistUntil=1893456000");
});
it("builds validation host from wildcard domain", async () => {
const service = new DnsPersistRecordService();
const record = await service.buildRecord({
domain: "*.example.com",
accountUri: "https://example.com/acct/1",
wildcard: true,
});
assert.equal(record.hostRecord, "_validation-persist");
});
it("builds relative validation host from subdomain", async () => {
const service = new DnsPersistRecordService();
const record = await service.buildRecord({
domain: "aaa.handsfree.work",
accountUri: "https://example.com/acct/1",
});
assert.equal(record.hostRecord, "_validation-persist.aaa");
assert.equal(record.mainDomain, "handsfree.work");
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
});
it("builds dns-persist record from acme account access", async () => {
const service = new DnsPersistRecordService();
(service as any).accessService = {
async getAccessById(id: number, checkUser: boolean, userId?: number) {
assert.equal(id, 12);
assert.equal(checkUser, true);
assert.equal(userId, 1);
return {
account: JSON.stringify({
accountKey: "private-key",
accountUri: "https://example.com/acct/1",
caType: "zerossl",
}),
};
},
};
const record = await service.buildRecordByAcmeAccount({
domain: "*.example.com",
caType: "zerossl",
acmeAccountAccessId: 12,
userId: 1,
projectId: 2,
});
assert.equal(record.domain, "example.com");
assert.equal(record.caType, "zerossl");
assert.equal(record.acmeAccountAccessId, 12);
assert.equal(record.accountUri, "https://example.com/acct/1");
assert.equal(record.hostRecord, "_validation-persist");
assert.equal(record.mainDomain, "example.com");
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
assert.equal(record.policy, "wildcard");
assert.equal(record.status, "pending");
});
it("rejects mismatched ca type", async () => {
const service = new DnsPersistRecordService();
(service as any).accessService = {
async getAccessById() {
return {
account: JSON.stringify({
accountKey: "private-key",
accountUri: "https://example.com/acct/1",
caType: "google",
}),
};
},
};
await assert.rejects(
() =>
service.buildRecordByAcmeAccount({
domain: "example.com",
caType: "zerossl",
acmeAccountAccessId: 12,
userId: 1,
}),
/颁发机构不匹配/
);
});
it("returns full local record after add and triggers auto create hook", async () => {
const service = new DnsPersistRecordService();
let saved: any = null;
let autoCreateId: number | null = null;
(service as any).repository = {
async save(param: any) {
param.id = 77;
saved = { ...param };
},
async findOneBy(where: any) {
return where.id === 77 ? saved : null;
},
async findOne() {
return null;
},
};
(service as any).accessService = {
async getAccessById() {
return {
account: JSON.stringify({
accountKey: "private-key",
accountUri: "https://example.com/acct/1",
caType: "letsencrypt",
}),
};
},
};
(service as any).tryAutoCreateDnsTxt = async (id: number) => {
autoCreateId = id;
};
const record: any = await service.add({
domain: "example.com",
mainDomain: "example.com",
caType: "letsencrypt",
acmeAccountAccessId: 1,
userId: 1,
projectId: 2,
});
assert.equal(autoCreateId, 77);
assert.equal(record.id, 77);
assert.equal(record.hostRecord, "_validation-persist");
assert.equal(record.mainDomain, "example.com");
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
assert.equal(record.status, "pending");
});
it("reuses existing record for the same domain and acme account", async () => {
const service = new DnsPersistRecordService();
let saveCount = 0;
const exists = {
id: 88,
domain: "example.com",
mainDomain: "example.com",
caType: "letsencrypt",
acmeAccountAccessId: 1,
userId: 1,
projectId: 2,
hostRecord: "_validation-persist",
recordValue: "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard",
policy: "wildcard",
status: "valid",
};
(service as any).repository = {
async save() {
saveCount++;
},
async findOne(options: any) {
return options.where.domain === "example.com" && options.where.acmeAccountAccessId === 1 ? exists : null;
},
};
(service as any).accessService = {
async getAccessById() {
return {
account: JSON.stringify({
accountKey: "private-key",
accountUri: "https://example.com/acct/1",
caType: "letsencrypt",
}),
};
},
};
const record: any = await service.add({
domain: "example.com",
mainDomain: "example.com",
caType: "letsencrypt",
acmeAccountAccessId: 1,
userId: 1,
projectId: 2,
});
assert.equal(saveCount, 0);
assert.equal(record.id, 88);
assert.equal(record.status, "valid");
});
it("upgrades existing non-wildcard record to wildcard and pending", async () => {
const service = new DnsPersistRecordService();
let saved: any = {
id: 89,
domain: "example.com",
mainDomain: "example.com",
caType: "letsencrypt",
acmeAccountAccessId: 1,
userId: 1,
projectId: 2,
hostRecord: "_validation-persist",
recordValue: "letsencrypt.org; accounturi=https://example.com/acct/1",
policy: null,
status: "valid",
recordRes: JSON.stringify({ old: true }),
};
(service as any).repository = {
async save(param: any) {
saved = { ...saved, ...param };
},
async findOne(options: any) {
return options.where.domain === "example.com" && options.where.acmeAccountAccessId === 1 ? saved : null;
},
async findOneBy(where: any) {
return where.id === 89 ? saved : null;
},
};
(service as any).accessService = {
async getAccessById() {
return {
account: JSON.stringify({
accountKey: "private-key",
accountUri: "https://example.com/acct/1",
caType: "letsencrypt",
}),
};
},
};
const record: any = await service.add({
domain: "example.com",
caType: "letsencrypt",
acmeAccountAccessId: 1,
userId: 1,
projectId: 2,
});
assert.equal(record.id, 89);
assert.equal(record.policy, "wildcard");
assert.equal(record.status, "pending");
assert.equal(record.mainDomain, "example.com");
assert.equal(record.recordValue, "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard");
assert.equal(record.recordRes, null);
});
it("returns manual cleanup hint when deleting record", async () => {
const service = new DnsPersistRecordService();
let deletedIds: any = null;
(service as any).repository = {
async findOneBy(where: any) {
return where.id === 90
? {
id: 90,
domain: "example.com",
mainDomain: "example.com",
hostRecord: "_validation-persist",
recordValue: "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard",
recordRes: null,
}
: null;
},
async delete(ids: any) {
deletedIds = ids;
},
};
await service.delete([90]);
assert.deepEqual(deletedIds.id._value, [90]);
assert.match(service.lastDeleteMessage, /请到域名供应商删除TXT记录/);
assert.match(service.lastDeleteMessage, /_validation-persist/);
});
it("triggers dns-persist verification asynchronously", async () => {
const service = new DnsPersistRecordService();
const savedStatuses: string[] = [];
let saved: any = {
id: 91,
domain: "example.com",
mainDomain: "example.com",
hostRecord: "_validation-persist",
recordValue: "letsencrypt.org; accounturi=https://example.com/acct/1; policy=wildcard",
status: "pending",
};
(service as any).repository = {
async findOneBy(where: any) {
return where.id === 91 ? saved : null;
},
async save(param: any) {
saved = { ...saved, ...param };
savedStatuses.push(saved.status);
},
};
(service as any).checkRecord = async () => true;
const triggered = await service.triggerVerify(91);
assert.equal(triggered, true);
assert.equal(saved.status, "validating");
await new Promise(resolve => setTimeout(resolve, 10));
assert.deepEqual(savedStatuses, ["validating", "valid"]);
assert.equal(saved.status, "valid");
});
});
@@ -0,0 +1,438 @@
import { BaseService } from "@certd/lib-server";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, Repository } from "typeorm";
import { createChallengeFn } from "@certd/acme-client";
import { AccessService } from "@certd/lib-server";
import { http, logger, utils } from "@certd/basic";
import { createDnsProvider, DomainParser } from "@certd/plugin-lib";
import { DnsPersistRecordEntity } from "../entity/dns-persist-record.js";
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
import { DomainEntity } from "../entity/domain.js";
export function buildDnsPersistRecordValue(req: { issuer?: string; accountUri: string; wildcard?: boolean; persistUntil?: number }) {
const parts = [req.issuer || "letsencrypt.org", `accounturi=${req.accountUri}`];
if (req.wildcard !== false) {
parts.push("policy=wildcard");
}
if (req.persistUntil) {
parts.push(`persistUntil=${req.persistUntil}`);
}
return parts.join("; ");
}
export type DnsPersistRecordBuildReq = {
domain: string;
caType?: string;
acmeAccountAccessId?: number;
commonAcmeAccountAccessId?: number;
wildcard?: boolean;
persistUntil?: number;
};
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class DnsPersistRecordService extends BaseService<DnsPersistRecordEntity> {
@InjectEntityModel(DnsPersistRecordEntity)
repository: Repository<DnsPersistRecordEntity>;
@InjectEntityModel(DomainEntity)
domainRepository: Repository<DomainEntity>;
@Inject()
accessService: AccessService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
lastDeleteMessage = "";
//@ts-ignore
getRepository() {
return this.repository;
}
normalizeDomain(domain: string) {
return domain?.replace(/^\*\./, "");
}
private async parseMainDomain(domain: string, userId?: number, projectId?: number) {
if (this.taskServiceBuilder && userId != null) {
const taskService = this.taskServiceBuilder.create({ userId, projectId });
const subDomainsGetter = await taskService.getSubDomainsGetter();
const domainParser = new DomainParser(subDomainsGetter, logger);
return await domainParser.parse(domain);
}
const parts = domain.split(".");
return parts.length > 2 ? parts.slice(-2).join(".") : domain;
}
private buildRelativeHostRecord(domain: string, mainDomain: string) {
let prefix = domain;
if (domain === mainDomain) {
prefix = "";
} else if (domain.endsWith(`.${mainDomain}`)) {
prefix = domain.substring(0, domain.length - mainDomain.length - 1);
}
return prefix ? `_validation-persist.${prefix}` : "_validation-persist";
}
private async buildFullHostRecord(record: Pick<DnsPersistRecordEntity, "domain" | "hostRecord" | "userId" | "projectId" | "mainDomain">) {
if (record.hostRecord === `_validation-persist.${record.domain}` || record.hostRecord.endsWith(`.${record.domain}`)) {
return record.hostRecord;
}
const mainDomain = record.mainDomain || (await this.parseMainDomain(record.domain, record.userId, record.projectId));
return `${record.hostRecord}.${mainDomain}`;
}
private parseAcmeAccount(account: string | any) {
if (!account) {
throw new Error("ACME账号授权缺少账号信息,请重新生成ACME账号");
}
const parsed = typeof account === "string" ? JSON.parse(account) : account;
if (!parsed.accountKey || !parsed.accountUri) {
throw new Error("ACME账号授权无效,请重新生成ACME账号");
}
return parsed;
}
async getAcmeAccount(req: DnsPersistRecordBuildReq & { userId: number; projectId?: number }) {
const accessId = req.acmeAccountAccessId || req.commonAcmeAccountAccessId;
if (!accessId) {
throw new Error("请选择ACME账号授权");
}
let access: any;
if (req.commonAcmeAccountAccessId) {
const entity = await this.accessService.info(accessId);
if (!entity || entity.userId !== 0 || entity.type !== "acmeAccount") {
throw new Error("公共ACME账号授权不存在");
}
access = await this.accessService.getAccessById(accessId, false);
} else {
access = await this.accessService.getAccessById(accessId, true, req.userId, req.projectId);
}
const account = this.parseAcmeAccount(access.account);
const caType = req.caType || account.caType;
if (caType && account.caType && caType !== account.caType) {
throw new Error("ACME账号授权与颁发机构不匹配");
}
return {
accessId,
caType,
account,
};
}
async buildRecord(req: { domain: string; accountUri: string; wildcard?: boolean; persistUntil?: number; userId?: number; projectId?: number }) {
const domain = this.normalizeDomain(req.domain);
const mainDomain = await this.parseMainDomain(domain, req.userId, req.projectId);
return {
mainDomain,
hostRecord: this.buildRelativeHostRecord(domain, mainDomain),
recordValue: buildDnsPersistRecordValue({
...req,
wildcard: true,
}),
};
}
async buildRecordByAcmeAccount(req: DnsPersistRecordBuildReq & { userId: number; projectId?: number }) {
const { account, caType, accessId } = await this.getAcmeAccount(req);
const record = await this.buildRecord({
domain: req.domain,
accountUri: account.accountUri,
wildcard: true,
persistUntil: req.persistUntil,
userId: req.userId,
projectId: req.projectId,
});
return {
...record,
domain: this.normalizeDomain(req.domain),
mainDomain: record.mainDomain,
caType,
acmeAccountAccessId: accessId,
accountUri: account.accountUri,
policy: "wildcard",
persistUntil: req.persistUntil,
status: "pending",
disabled: false,
};
}
async add(param: any) {
const record = await this.buildRecordByAcmeAccount(param);
const exists = await this.findOne({
where: {
domain: record.domain,
caType: record.caType,
acmeAccountAccessId: record.acmeAccountAccessId,
userId: param.userId,
projectId: param.projectId,
},
});
if (exists) {
if (exists.policy !== "wildcard") {
await this.upgradeToWildcardRecord(exists, record);
return await this.info(exists.id);
}
if (exists.status !== "valid" && !exists.dnsProviderAccess) {
await this.tryAutoCreateDnsTxt(exists.id);
return await this.info(exists.id);
}
return exists;
}
const result = await super.add({
...param,
...record,
});
await this.tryAutoCreateDnsTxt(result.id);
return await this.info(result.id);
}
async update(param: any) {
const old = await this.info(param.id);
if (!old) {
throw new Error("DNS持久验证记录不存在");
}
if (param.domain || param.caType || param.acmeAccountAccessId || param.commonAcmeAccountAccessId || param.persistUntil != null || param.wildcard != null) {
const record = await this.buildRecordByAcmeAccount({
domain: param.domain || old.domain,
caType: param.caType || old.caType,
acmeAccountAccessId: param.acmeAccountAccessId || old.acmeAccountAccessId,
commonAcmeAccountAccessId: param.commonAcmeAccountAccessId,
wildcard: true,
persistUntil: param.persistUntil ?? old.persistUntil,
userId: old.userId,
projectId: old.projectId,
});
param = {
...param,
...record,
};
}
await super.update(param);
if (param.domain || param.caType || param.acmeAccountAccessId || param.commonAcmeAccountAccessId || param.persistUntil != null || param.wildcard != null) {
await this.tryAutoCreateDnsTxt(param.id);
}
}
async checkRecord(req: { hostRecord: string; recordValue: string }) {
const { walkTxtRecord } = createChallengeFn();
const values = await walkTxtRecord(req.hostRecord);
return values.includes(req.recordValue);
}
async getByDomain(req: DnsPersistRecordBuildReq & { userId: number; projectId?: number; createOnNotFound?: boolean }) {
const account = await this.getAcmeAccount(req);
const domain = this.normalizeDomain(req.domain);
let record = await this.findOne({
where: {
domain,
caType: account.caType,
acmeAccountAccessId: account.accessId,
userId: req.userId,
projectId: req.projectId,
},
});
if (!record && req.createOnNotFound) {
record = await this.add({
...req,
domain,
caType: account.caType,
acmeAccountAccessId: account.accessId,
});
} else if (record && record.policy !== "wildcard") {
const wildcardRecord = await this.buildRecordByAcmeAccount({
...req,
domain,
caType: account.caType,
acmeAccountAccessId: account.accessId,
persistUntil: req.persistUntil ?? record.persistUntil,
});
await this.upgradeToWildcardRecord(record, wildcardRecord);
record = await this.info(record.id);
} else if (record && record.status !== "valid" && !record.dnsProviderAccess) {
await this.tryAutoCreateDnsTxt(record.id);
record = await this.info(record.id);
}
return record;
}
private async upgradeToWildcardRecord(exists: DnsPersistRecordEntity, wildcardRecord: Partial<DnsPersistRecordEntity>) {
await super.update({
id: exists.id,
hostRecord: wildcardRecord.hostRecord,
recordValue: wildcardRecord.recordValue,
mainDomain: wildcardRecord.mainDomain,
policy: "wildcard",
persistUntil: wildcardRecord.persistUntil ?? exists.persistUntil,
status: "pending",
recordRes: null,
});
}
async verify(id: number) {
const record = await this.info(id);
if (!record) {
throw new Error("DNS持久验证记录不存在");
}
const ok = await this.checkRecord({
hostRecord: await this.buildFullHostRecord(record),
recordValue: record.recordValue,
});
await this.update({
id: record.id,
status: ok ? "valid" : "failed",
});
return ok;
}
async triggerVerify(id: number) {
await super.update({
id,
status: "validating",
});
setTimeout(() => {
this.verify(id).catch(async (e: any) => {
logger.error(`DNS持久验证记录后台校验失败:${e.message || e}`);
await super.update({
id,
status: "failed",
});
});
}, 0);
return true;
}
private async findDomainDnsProvider(record: DnsPersistRecordEntity) {
const taskService = this.taskServiceBuilder.create({ userId: record.userId, projectId: record.projectId });
const subDomainsGetter = await taskService.getSubDomainsGetter();
const domainParser = new DomainParser(subDomainsGetter, logger);
const mainDomain = record.mainDomain || (await domainParser.parse(record.domain));
const domains = [...new Set([record.domain, mainDomain].filter(Boolean))];
const list = await this.domainRepository.find({
where: {
domain: In(domains),
userId: record.userId,
projectId: record.projectId,
challengeType: "dns",
disabled: false,
},
});
const matched = list.find(item => item.domain === record.domain) || list.find(item => item.domain === mainDomain);
if (!matched) {
return null;
}
return {
dnsProviderType: matched.dnsProviderType,
dnsProviderAccess: matched.dnsProviderAccess,
};
}
private async resolveDnsProvider(record: DnsPersistRecordEntity, req: { dnsProviderType?: string; dnsProviderAccess?: number }) {
if (req.dnsProviderType && req.dnsProviderAccess) {
return {
dnsProviderType: req.dnsProviderType,
dnsProviderAccess: req.dnsProviderAccess,
};
}
const provider = await this.findDomainDnsProvider(record);
if (!provider) {
throw new Error("未找到该域名在域名管理中的DNS授权配置,请手动选择DNS服务商和授权");
}
return provider;
}
private async tryAutoCreateDnsTxt(id: number) {
const record = await this.info(id);
if (!record || record.status === "valid") {
return;
}
const provider = await this.findDomainDnsProvider(record);
if (!provider) {
return;
}
try {
await this.createDnsTxt({
id,
...provider,
});
} catch (e: any) {
await super.update({
id,
status: "failed",
recordRes: JSON.stringify({
autoCreateError: e.message || `${e}`,
}),
});
}
}
async createDnsTxt(req: { id: number; dnsProviderType?: string; dnsProviderAccess?: number }) {
const record = await this.info(req.id);
if (!record) {
throw new Error("DNS持久验证记录不存在");
}
const provider = await this.resolveDnsProvider(record, req);
const taskService = this.taskServiceBuilder.create({ userId: record.userId, projectId: record.projectId });
const subDomainsGetter = await taskService.getSubDomainsGetter();
const domainParser = new DomainParser(subDomainsGetter, logger);
const access = await this.accessService.getAccessById(provider.dnsProviderAccess, true, record.userId, record.projectId);
const dnsProvider = await createDnsProvider({
dnsProviderType: provider.dnsProviderType,
context: {
access,
logger,
http,
utils,
domainParser,
serviceGetter: taskService,
},
});
const mainDomain = record.mainDomain || (await domainParser.parse(record.domain));
const fullRecordRaw = await this.buildFullHostRecord(record);
const fullRecord = dnsProvider.usePunyCode() ? fullRecordRaw : dnsProvider.punyCodeDecode(fullRecordRaw);
let hostRecord = fullRecord.replace(`${mainDomain}`, "");
if (hostRecord.endsWith(".")) {
hostRecord = hostRecord.substring(0, hostRecord.length - 1);
}
const recordReq = {
domain: mainDomain,
fullRecord,
hostRecord,
type: "TXT",
value: record.recordValue,
};
const recordRes = await dnsProvider.createRecord(recordReq);
const verified = await this.checkRecord({
hostRecord: await this.buildFullHostRecord(record),
recordValue: record.recordValue,
});
await this.update({
id: record.id,
dnsProviderType: provider.dnsProviderType,
dnsProviderAccess: provider.dnsProviderAccess,
recordRes: JSON.stringify({ recordReq, recordRes }),
status: verified ? "valid" : "validating",
});
return {
recordReq,
recordRes,
verified,
};
}
async delete(ids: string | any[], where?: any) {
const idList = this.resolveIdArr(ids);
const messages: string[] = [];
for (const id of idList) {
const record = await this.info(id);
if (!record) {
continue;
}
messages.push(`DNS持久验证记录已删除,请到域名供应商删除TXT记录:${record.hostRecord} => ${record.recordValue}`);
}
this.lastDeleteMessage = messages.join("\n");
return await super.delete(ids, where);
}
}
@@ -363,7 +363,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!old && userSuite?.pipelineCount.max != -1 && userSuite?.pipelineCount.used + 1 > userSuite?.pipelineCount.max) {
throw new NeedSuiteException(`对不起,您最多只能创建${userSuite?.pipelineCount.max}条流水线,请购买或升级套餐`);
}
let oldDomainCount = 0;
let oldWildcardDomainCount = 0;
if (old?.id) {
@@ -0,0 +1,159 @@
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { CertApplyPluginNames } from '@certd/plugin-cert';
import { CertInfo, createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from '@certd/plugin-lib';
import { AliyunAccess } from '../../../plugin-lib/aliyun/access/index.js';
import { AliyunSslClient, CasCertId } from '../../../plugin-lib/aliyun/lib/index.js';
@IsTaskPlugin({
name: 'DeployCertToAliyunLive',
title: '阿里云-部署至直播(Live',
icon: 'svg:icon-aliyun',
group: pluginGroups.aliyun.key,
desc: '部署证书到阿里云视频直播(Live)域名',
needPlus: false,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DeployCertToAliyunLive extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames, 'uploadCertToAliyun'],
},
template: false,
required: true,
})
cert!: CertInfo | CasCertId;
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
certDomains!: string[];
@TaskInput({
title: 'Access授权',
helper: '阿里云授权AccessKeyId、AccessKeySecret',
component: {
name: 'access-selector',
type: 'aliyun',
},
required: true,
})
accessId!: string;
@TaskInput({
title: '证书服务接入点',
helper: '不会选就按默认',
value: 'cas.aliyuncs.com',
component: {
name: 'a-select',
options: [
{ value: 'cas.aliyuncs.com', label: '中国大陆' },
{ value: 'cas.ap-southeast-1.aliyuncs.com', label: '新加坡' },
{ value: 'cas.eu-central-1.aliyuncs.com', label: '德国(法兰克福)' },
],
},
required: true,
})
endpoint!: string;
@TaskInput(
createRemoteSelectInputDefine({
title: '直播域名',
helper: '请选择要部署证书的直播域名',
typeName: 'DeployCertToAliyunLive',
action: DeployCertToAliyunLive.prototype.onGetDomainList.name,
watches: ['certDomains', 'accessId'],
pager: true,
search: true,
})
)
domainList!: string[];
async onInstance() {}
async execute(): Promise<void> {
this.logger.info('开始部署证书到阿里云直播');
const access = await this.getAccess<AliyunAccess>(this.accessId);
if (this.cert == null) {
throw new Error('域名证书参数为空,请检查前置任务');
}
const client = await this.getClient(access);
const sslClient = new AliyunSslClient({
access,
logger: this.logger,
endpoint: this.endpoint || 'cas.aliyuncs.com',
});
// 确保证书已上传到 CAS,统一使用 cas 方式部署
const casCert = await sslClient.uploadCertOrGet(this.cert);
// const certName = this.appendTimeSuffix(this.certName || casCert.certName);
for (const domain of this.domainList) {
const res = await client.doRequest({
action: 'SetLiveDomainCertificate',
version: '2016-11-01',
protocol: 'HTTPS',
data: {
query: {
DomainName: domain,
CertName: casCert.certName,
CertType: 'cas',
SSLProtocol: 'on',
CertId: casCert.certId,
},
},
});
this.logger.info('部署直播域名[' + domain + ']证书成功:' + JSON.stringify(res));
}
}
async getClient(access: AliyunAccess) {
const endpoint = 'live.aliyuncs.com';
return access.getClient(endpoint);
}
async onGetDomainList(data: PageSearch) {
if (!this.accessId) {
throw new Error('请选择Access授权');
}
const access = await this.getAccess<AliyunAccess>(this.accessId);
const client = await this.getClient(access);
const res = await client.doRequest({
action: 'DescribeLiveUserDomains',
version: '2016-11-01',
protocol: 'HTTPS',
data: {
query: {
DomainName: data.searchKey || undefined,
PageNumber: data.pageNo || 1,
PageSize: data.pageSize || 50,
},
},
});
const list = res?.Domains?.PageData;
if (!list || list.length === 0) {
throw new Error('没有找到直播域名,请先在阿里云添加直播域名');
}
const options = list.map((item: any) => {
return {
label: item.DomainName,
value: item.DomainName,
domain: item.DomainName,
};
});
return this.ctx.utils.options.buildGroupOptions(options, this.certDomains);
}
}
new DeployCertToAliyunLive();
@@ -1,4 +1,4 @@
export * from './deploy-to-cdn/index.js';
export * from './deploy-to-cdn/index.js';
export * from './deploy-to-dcdn/index.js';
export * from './deploy-to-oss/index.js';
export * from './upload-to-aliyun/index.js';
@@ -10,8 +10,9 @@ export * from './deploy-to-fc/index.js';
export * from './deploy-to-esa/index.js';
export * from './deploy-to-ga/index.js';
export * from './deploy-to-vod/index.js';
export * from './deploy-to-live/index.js';
export * from './deploy-to-apigateway/index.js';
export * from './deploy-to-apig/index.js';
export * from './deploy-to-ack/index.js';
export * from './deploy-to-all/index.js';
export * from './delete-expiring-cert/index.js';
export * from './delete-expiring-cert/index.js';
@@ -0,0 +1,59 @@
import assert from "assert";
import { AcmeAccountAccess } from "./acme-account-access.js";
import { AcmeService } from "../plugin/cert-plugin/acme.js";
describe("AcmeAccountAccess", () => {
it("requires generated account payload before use", () => {
const access = new AcmeAccountAccess();
assert.throws(() => access.getAccount(), /ACME账号信息无效/);
});
it("parses generated account payload", () => {
const access = new AcmeAccountAccess();
access.account = JSON.stringify({
accountKey: "private-key",
accountUri: "https://example.com/acct/1",
caType: "letsencrypt",
email: "user@example.com",
directoryUrl: "https://example.com/directory",
});
const account = access.getAccount();
assert.equal(account.accountKey, "private-key");
assert.equal(account.accountUri, "https://example.com/acct/1");
});
it("generates account payload through acme service", async () => {
const original = AcmeService.prototype.getAcmeClient;
const calls: string[] = [];
AcmeService.prototype.getAcmeClient = async function (email: string) {
calls.push(email);
await this.userContext.setObj(this.buildAccountKey(email), { key: "generated-key" });
return {
getAccountUrl() {
return "https://example.com/acct/2";
},
} as any;
};
try {
const access = new AcmeAccountAccess();
access.caType = "google";
access.email = "user@example.com";
access.eabKid = "kid-1";
access.eabHmacKey = "hmac-1";
const account = JSON.parse(await access.onGenerateAccount());
assert.equal(calls[0], "user@example.com");
assert.equal(account.accountKey, "generated-key");
assert.equal(account.accountUri, "https://example.com/acct/2");
assert.equal(account.caType, "google");
assert.equal(account.email, "user@example.com");
} finally {
AcmeService.prototype.getAcmeClient = original;
}
});
});
@@ -0,0 +1,239 @@
import { AccessInput, BaseAccess, IsAccess } from "@certd/pipeline";
import * as acme from "@certd/acme-client";
import { AcmeService } from "../plugin/cert-plugin/acme.js";
export type AcmeAccountInfo = {
accountKey: string;
accountUri: string;
caType: string;
email: string;
directoryUrl: string;
eab?: {
kid?: string;
hmacKey?: string;
usedAt: number;
};
};
function parseAccount(account?: string | AcmeAccountInfo): AcmeAccountInfo | null {
if (!account) {
return null;
}
if (typeof account !== "string") {
return account;
}
return JSON.parse(account);
}
@IsAccess({
name: "acmeAccount",
title: "ACME账号",
desc: "用于复用ACME账号私钥和账号地址,证书申请时不再临时创建账号",
icon: "ph:certificate",
subtype: "caType",
} as any)
export class AcmeAccountAccess extends BaseAccess {
@AccessInput({
title: "颁发机构",
component: {
name: "a-select",
options: [
{ value: "letsencrypt", label: "Let's Encrypt" },
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境" },
{ value: "google", label: "Google" },
{ value: "zerossl", label: "ZeroSSL" },
{ value: "litessl", label: "litessl" },
{ value: "sslcom", label: "SSL.com" },
],
},
required: true,
mergeScript: `
return {
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
}
}
`,
})
caType = "letsencrypt";
@AccessInput({
title: "邮箱",
component: {
placeholder: "user@example.com",
},
rules: [{ type: "email", message: "请输入正确的邮箱" }],
required: true,
mergeScript: `
return {
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
}
}
`,
})
email = "";
@AccessInput({
title: "ACME Directory URL",
component: {
placeholder: "自定义ACME服务端点",
},
helper: "自定义ACME时必填,其他颁发机构默认自动使用内置端点",
required: false,
mergeScript: `
return {
show: false,
}
`,
})
directoryUrl = "";
@AccessInput({
title: "EAB KID",
component: {
placeholder: "需要EAB的颁发机构生成账号时填写",
},
helper:
"需要提供EAB授权" +
"\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'" +
"\nGoogle:请查看[google获取eab帮助文档](https://certd.docmirror.cn/guide/use/google/),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱" +
"\nSSL.com:[SSL.com账号页面](https://secure.ssl.com/account),然后点击api credentials链接,然后点击编辑按钮,查看Secret key和HMAC key" +
"\nlitessl:[litesslEAB页面](https://freessl.cn/automation/eab-manager),然后点击新增EAB",
required: false,
encrypt: true,
mergeScript: `
return {
show: ctx.compute(({form})=>{
const caType = form.access?.caType;
return ['google','zerossl','sslcom','litessl'].includes(caType);
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
}
}
`,
})
eabKid = "";
@AccessInput({
title: "EAB HMAC Key",
component: {
placeholder: "需要EAB的颁发机构生成账号时填写",
},
required: false,
encrypt: true,
mergeScript: `
return {
show: ctx.compute(({form})=>{
const caType = form.access?.caType;
return ['google','zerossl','sslcom','litessl'].includes(caType);
}),
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
}
}
`,
})
eabHmacKey = "";
@AccessInput({
title: "ACME账号信息",
component: {
name: "refresh-input",
action: "GenerateAccount",
buttonText: "生成ACME账号",
successMessage: "ACME账号已生成,请保存授权配置",
},
required: true,
helper: "请生成ACME账号,账号一旦生成不允许修改",
encrypt: true,
mergeScript: `
return {
component: {
disabled: ctx.compute(({form})=> !!form.access?.account)
}
}
`,
})
account = "";
getDirectoryUrl() {
if (this.caType === "custom") {
if (!this.directoryUrl) {
throw new Error("自定义ACME需要填写Directory URL");
}
return this.directoryUrl;
}
return acme.getDirectoryUrl({ sslProvider: this.caType, pkType: "rsa_2048" });
}
async onGenerateAccount() {
if (!this.caType) {
throw new Error("请先选择颁发机构");
}
if (!this.email) {
throw new Error("请先填写邮箱");
}
const needEab = ["google", "zerossl", "sslcom", "litessl"].includes(this.caType);
if (needEab && (!this.eabKid || !this.eabHmacKey)) {
throw new Error("该颁发机构需要填写EAB KID和EAB HMAC Key后才能生成账号");
}
const account = await this.createAccountInfo();
return JSON.stringify(account);
}
private async createAccountInfo(): Promise<AcmeAccountInfo> {
const directoryUrl = this.getDirectoryUrl();
const externalAccountBinding = this.eabKid && this.eabHmacKey ? { kid: this.eabKid, hmacKey: this.eabHmacKey } : undefined;
const memoryStore = new Map<string, any>();
const userContext = {
async getObj(key: string) {
return memoryStore.get(key);
},
async setObj(key: string, value: any) {
memoryStore.set(key, value);
},
};
const acmeService = new AcmeService({
userId: 0,
userContext: userContext as any,
logger: (this.ctx?.logger || console) as any,
sslProvider: this.caType as any,
eab: externalAccountBinding ? ({ ...externalAccountBinding, id: 0 } as any) : undefined,
privateKeyType: "rsa_2048",
signal: (this.ctx as any)?.signal,
maxCheckRetryCount: 20,
domainParser: {} as any,
});
const client = await acmeService.getAcmeClient(this.email);
const conf = await userContext.getObj(acmeService.buildAccountKey(this.email));
if (!conf?.key || !client.getAccountUrl()) {
throw new Error("ACME账号生成失败,请稍后重试");
}
const account: AcmeAccountInfo = {
accountKey: conf.key,
accountUri: client.getAccountUrl(),
caType: this.caType,
email: this.email,
directoryUrl,
};
if (externalAccountBinding) {
account.eab = {
...externalAccountBinding,
usedAt: Date.now(),
};
}
return account;
}
getAccount(): AcmeAccountInfo {
const account = parseAccount(this.account);
if (!account?.accountKey || !account?.accountUri) {
throw new Error("ACME账号信息无效,请重新生成ACME账号");
}
return account;
}
}
new AcmeAccountAccess();
@@ -1,2 +1,3 @@
export * from "./eab-access.js";
export * from "./google-access.js";
export * from "./acme-account-access.js";
@@ -1,4 +1,5 @@
import assert from "assert";
import { utils } from "@certd/basic";
import { AcmeService } from "./acme.js";
const logger = {
@@ -173,4 +174,28 @@ describe("AcmeService challenge", () => {
assert.deepEqual(parseCalls, ["www.example.com", "certd-key.cname.sub.example.com"]);
});
it("enables proxy mapping when acme directory request fails", async () => {
const originalRequest = utils.http.request;
utils.http.request = async () => {
throw new Error("timeout");
};
try {
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "google",
domainParser: {} as any,
});
const urlMapping = await service.resolveUrlMapping("https://dv.acme-v02.api.pki.goog/directory");
assert.equal(urlMapping.enabled, true);
assert.equal(urlMapping.mappings["dv.acme-v02.api.pki.goog"], "gg.px.certd.handfree.work");
} finally {
utils.http.request = originalRequest;
}
});
});
@@ -21,15 +21,30 @@ export type HttpVerifyPlan = {
export type DomainVerifyPlan = {
domain: string;
mainDomain: string;
type: "cname" | "dns" | "http";
type: "cname" | "dns" | "http" | "dns-persist";
dnsProvider?: IDnsProvider;
cnameVerifyPlan?: CnameVerifyPlan;
httpVerifyPlan?: HttpVerifyPlan;
dnsPersistVerifyPlan?: DnsPersistVerifyPlan;
};
export type DomainsVerifyPlan = {
[key: string]: DomainVerifyPlan;
};
export type AcmeAccountInfo = {
accountKey: string;
accountUri: string;
caType: SSLProvider | string;
email: string;
directoryUrl: string;
};
export type DnsPersistVerifyPlan = {
hostRecord: string;
recordValue: string;
accountUri: string;
};
export type Providers = {
dnsProvider?: IDnsProvider;
domainsVerifyPlan?: DomainsVerifyPlan;
@@ -38,7 +53,7 @@ export type Providers = {
export type CertInfo = {
crt: string; //fullchain证书
key: string; //私钥
csr: string; //csr
csr?: string; //csr
oc?: string; //仅证书,非fullchain证书
ic?: string; //中间证书
pfx?: string;
@@ -153,8 +168,7 @@ export class AcmeService {
await this.userContext.setObj(this.buildAccountKey(email), conf);
}
async getAcmeClient(email: string): Promise<acme.Client> {
const directoryUrl = acme.getDirectoryUrl({ sslProvider: this.sslProvider, pkType: this.options.privateKeyType });
buildUrlMapping(directoryUrl: string): UrlMapping {
let targetUrl = directoryUrl.replace("https://", "");
targetUrl = targetUrl.substring(0, targetUrl.indexOf("/"));
@@ -174,10 +188,29 @@ export class AcmeService {
if (this.options.reverseProxy && targetUrl) {
mappings[targetUrl] = this.options.reverseProxy;
}
const urlMapping: UrlMapping = {
return {
enabled: false,
mappings,
};
}
async resolveUrlMapping(directoryUrl: string) {
const urlMapping = this.buildUrlMapping(directoryUrl);
if (this.options.useMappingProxy) {
urlMapping.enabled = true;
return urlMapping;
}
const isOk = await this.testDirectory(directoryUrl);
if (!isOk) {
this.logger.info("测试访问失败,自动使用代理");
urlMapping.enabled = true;
}
return urlMapping;
}
async getAcmeClient(email: string): Promise<acme.Client> {
const directoryUrl = acme.getDirectoryUrl({ sslProvider: this.sslProvider, pkType: this.options.privateKeyType });
const urlMapping = await this.resolveUrlMapping(directoryUrl);
const conf = await this.getAccountConfig(email, urlMapping);
if (conf.key == null) {
conf.key = await this.createNewKey();
@@ -185,16 +218,6 @@ export class AcmeService {
this.logger.info(`创建新的Accountkey:${email}`);
}
if (this.options.useMappingProxy) {
urlMapping.enabled = true;
} else {
//测试directory是否可以访问
const isOk = await this.testDirectory(directoryUrl);
if (!isOk) {
this.logger.info("测试访问失败,自动使用代理");
urlMapping.enabled = true;
}
}
const client = new acme.Client({
sslProvider: this.sslProvider,
directoryUrl: directoryUrl,
@@ -238,6 +261,26 @@ export class AcmeService {
return client;
}
async getAcmeClientByAccount(account: AcmeAccountInfo): Promise<acme.Client> {
if (!account?.accountKey || !account?.accountUri) {
throw new Error("ACME账号信息无效,请重新生成ACME账号");
}
const directoryUrl = account.directoryUrl || acme.getDirectoryUrl({ sslProvider: account.caType, pkType: this.options.privateKeyType });
const urlMapping = await this.resolveUrlMapping(directoryUrl);
return new acme.Client({
sslProvider: account.caType,
directoryUrl,
accountKey: account.accountKey,
accountUrl: account.accountUri,
backoffAttempts: this.options.maxCheckRetryCount || 20,
backoffMin: 5000,
backoffMax: 30 * 1000,
urlMapping,
signal: this.options.signal,
logger: this.logger,
});
}
async createNewKey() {
const key = await acme.crypto.createPrivateKey(2048);
return key.toString();
@@ -300,6 +343,18 @@ export class AcmeService {
};
};
const doDnsPersistVerify = async (challenge: any, plan: DnsPersistVerifyPlan) => {
if (challenge == null) {
throw new Error("该域名不支持dns-persist-01方式校验,请确认当前CA是否已开放该能力");
}
this.logger.info("DNS持久验证");
challenge.expectedRecordValue = plan.recordValue;
return {
challenge,
keyAuthorization: "",
};
};
let dnsProvider = providers.dnsProvider;
let fullRecord = `_acme-challenge.${fullDomain}`;
@@ -343,6 +398,9 @@ export class AcmeService {
} else {
throw new Error("未找到域名【" + fullDomain + "】的http校验配置");
}
} else if (domainVerifyPlan.type === "dns-persist") {
checkIpChallenge("dns-persist");
return await doDnsPersistVerify(getChallenge("dns-persist-01"), domainVerifyPlan.dnsPersistVerifyPlan);
} else {
throw new Error("不支持的校验类型", domainVerifyPlan.type);
}
@@ -394,6 +452,8 @@ export class AcmeService {
this.logger.error("删除解析记录出错:", e);
throw e;
}
} else if (challenge.type === "dns-persist-01") {
this.logger.info(`DNS持久验证无需清理:${fullDomain}`);
}
}
@@ -407,9 +467,10 @@ export class AcmeService {
privateKeyType?: string;
profile?: string;
preferredChain?: string;
acmeAccount?: AcmeAccountInfo;
}): Promise<CertInfo> {
const { email, csrInfo, dnsProvider, domainsVerifyPlan, profile, preferredChain } = options;
const client: acme.Client = await this.getAcmeClient(email);
const { email, csrInfo, dnsProvider, domainsVerifyPlan, profile, preferredChain, acmeAccount } = options;
const client: acme.Client = acmeAccount ? await this.getAcmeClientByAccount(acmeAccount) : await this.getAcmeClient(email);
let domains = options.domains;
const encodingDomains = [];
@@ -463,12 +524,13 @@ export class AcmeService {
domainsVerifyPlan,
};
/* 自动申请证书 */
const challengePriority = domainsVerifyPlan && Object.values(domainsVerifyPlan).some((item: any) => item?.type === "dns-persist") ? ["dns-persist-01"] : ["dns-01", "http-01"];
const crt = await client.auto({
csr,
email: email,
termsOfServiceAgreed: true,
skipChallengeVerification: this.skipLocalVerify,
challengePriority: ["dns-01", "http-01"],
challengePriority,
challengeCreateFn: async (
authz: acme.Authorization,
keyAuthorizationGetter: (challenge: Challenge) => Promise<string>

Some files were not shown because too many files have changed in this diff Show More