Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev

This commit is contained in:
xiaojunnuo
2026-05-11 09:40:22 +08:00
80 changed files with 2037 additions and 333 deletions
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/publishlab/node-acme-client/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/publishlab/node-acme-client/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/publishlab/node-acme-client/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+6 -4
View File
@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"module": "./dist/index.js",
"main": "./dist/index.js",
@@ -18,7 +18,7 @@
"types"
],
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.39.13",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.9.0",
@@ -35,10 +35,12 @@
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.2",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.6.0",
"nock": "^13.5.4",
@@ -55,7 +57,7 @@
"prepublishOnly": "npm run build && npm run build-docs",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
"before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
"test:unit": "npm run before-test:unit && tsc -p tsconfig.test.json --skipLibCheck && mocha -t 60000 \"dist-test/**/*.test.js\"",
"test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -74,5 +76,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+1 -1
View File
@@ -1 +1 @@
23:06
00:21
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/basic",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -13,7 +13,7 @@
"dev-build": "npm run build",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"test:unit": "mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -40,9 +40,11 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "4.3.10",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -50,5 +52,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
@@ -26,7 +26,7 @@ export class HttpError extends Error {
return;
}
let message = error?.message || error?.response.statusText || error?.code;
let message = error?.message || error?.response?.statusText || error?.code;
if (message && typeof message === "string" && message.indexOf) {
for (const key in errorMap) {
if (message.indexOf(key) > -1) {
@@ -267,7 +267,7 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
logger.error(`请求出错:${errorMessage} status:${status},statusText:${error.response?.statusText || error.code},url:${error.config?.url},method:${error.config?.method}`);
logger.error("返回数据:", JSON.stringify(error.response?.data));
if (error.response?.data) {
const message = error.response.data.message || error.response.data.msg || error.response.data.error;
const message = error.response?.data?.message || error.response?.data?.msg || error.response?.data?.error;
if (typeof message === "string") {
error.message = message;
}
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+7 -5
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,13 +14,13 @@
"build3": "rollup -c",
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm",
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/basic": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"reflect-metadata": "^0.1.13"
@@ -37,9 +37,11 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "4.3.10",
"cross-env": "^7.0.3",
"eslint": "^8.41.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -47,5 +49,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
@@ -3,6 +3,7 @@ import { IAccess } from "../access/index.js";
export type CnameProvider = {
id: any;
domain: string;
subdomain?: string;
title?: string;
dnsProviderType?: string;
access?: IAccess;
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-huawei
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-huawei",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
"types": "./dist/d/index.d.ts",
@@ -11,7 +11,7 @@
"build": "npm run before-build && rollup -c ",
"dev-build": "npm run build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"dependencies": {
@@ -22,8 +22,10 @@
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-iframe
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-iframe",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,7 +14,7 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"dependencies": {
@@ -24,13 +24,15 @@
"@types/chai": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/jdcloud
+5 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/jdcloud",
"version": "1.39.12",
"version": "1.39.13",
"description": "jdcloud openApi sdk",
"main": "./dist/bundle.js",
"module": "./dist/bundle.js",
@@ -8,7 +8,7 @@
"scripts": {
"build": "rollup -c ",
"dev-build": "npm run build",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish"
},
"author": "",
@@ -30,7 +30,8 @@
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.1.2",
"config": "^1.30.0",
"cross-env": "^5.1.4",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"js-yaml": "^3.11.0",
"mocha": "^5.0.0",
"prettier": "^2.8.8",
@@ -57,5 +58,5 @@
"fetch"
]
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/lib-k8s
+6 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -14,25 +14,27 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/basic": "^1.39.12",
"@certd/basic": "^1.39.13",
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+7
View File
@@ -3,6 +3,13 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Performance Improvements
+10 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/lib-server",
"version": "1.39.12",
"version": "1.39.13",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -12,7 +12,7 @@
"build": "npm run before-build && tsc --skipLibCheck",
"dev-build": "npm run build",
"test": "midway-bin test --ts -V",
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
"cov": "midway-bin cov --ts",
"lint": "mwts check",
@@ -29,11 +29,11 @@
],
"license": "AGPL",
"dependencies": {
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plugin-lib": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"@midwayjs/cache": "3.14.0",
"@midwayjs/core": "3.20.11",
"@midwayjs/i18n": "3.20.13",
@@ -44,7 +44,6 @@
"@midwayjs/upload": "3.20.13",
"@midwayjs/validate": "3.20.13",
"better-sqlite3": "^11.1.2",
"cross-env": "^7.0.3",
"dayjs": "^1.11.7",
"lodash-es": "^4.17.21",
"mwts": "^1.3.0",
@@ -57,9 +56,11 @@
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -68,5 +69,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
@@ -0,0 +1,30 @@
import assert from "assert";
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.decryptAccessEntity = () => ({
kid: "kid-1",
});
service.update = async (param: any) => {
updateParam = param;
return param;
};
await service.updateAccess({
id: 12,
accountKey: "account-key",
});
assert.deepEqual(JSON.parse(updateParam.setting), {
kid: "kid-1",
accountKey: "account-key",
});
});
});
@@ -123,6 +123,25 @@ export class AccessService extends BaseService<AccessEntity> {
return await super.update(param);
}
async updateAccess(access: any) {
const oldEntity = await this.info(access.id);
if (oldEntity == null) {
throw new ValidateException('该授权配置不存在,请确认是否已被删除');
}
const setting = this.decryptAccessEntity(oldEntity);
for (const key of Object.keys(access)) {
if (key === 'id') {
continue;
}
setting[key] = access[key];
}
return await this.update({
id: access.id,
type: oldEntity.type,
setting: JSON.stringify(setting),
});
}
async getSimpleInfo(id: number) {
const entity = await this.info(id);
if (entity == null) {
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/midway-flyway-js
+5 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.39.12",
"version": "1.39.13",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",
@@ -13,7 +13,7 @@
"dev-build": "npm run build",
"test": "midway-bin test --ts -V",
"test1": "midway-bin test --ts -V -f test/blank.test.ts -t 'hash-check'",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"cov": "midway-bin cov --ts",
"prepublish": "npm run build",
"pub": "npm publish"
@@ -36,16 +36,18 @@
"@types/node": "^18",
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"tslib": "^2.8.1",
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/plugin-cert
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,15 +13,15 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
"dependencies": {
"@certd/acme-client": "^1.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plugin-lib": "^1.39.13",
"psl": "^1.9.0",
"punycode.js": "^2.3.1"
},
@@ -31,13 +31,15 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Performance Improvements
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
**Note:** Version bump only for package @certd/plugin-lib
+9 -7
View File
@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-lib",
"private": false,
"version": "1.39.12",
"version": "1.39.13",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,7 +13,7 @@
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test:unit": "mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"test:unit": "cross-env NODE_ENV=unittest mocha --no-config --node-option no-warnings --node-option loader=ts-node/esm \"src/**/*.test.ts\"",
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch"
},
@@ -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.39.12",
"@certd/basic": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"@kubernetes/client-node": "0.21.0",
"ali-oss": "^6.22.0",
"basic-ftp": "^5.0.5",
@@ -50,14 +50,16 @@
"@typescript-eslint/eslint-plugin": "^8.26.1",
"@typescript-eslint/parser": "^8.26.1",
"chai": "^4.3.6",
"cross-env": "^7.0.3",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"esmock": "^2.7.5",
"mocha": "^10.1.0",
"prettier": "^2.8.8",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
"gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
}
+7 -2
View File
@@ -1,4 +1,4 @@
FROM node:22-alpine AS builder
FROM node:22-alpine3.21 AS builder
# RUN apk add build-base
# RUN wget -O - https://github.com/jemalloc/jemalloc/releases/download/5.3.0/jemalloc-5.3.0.tar.bz2 | tar -xj && \
@@ -18,9 +18,14 @@ RUN npm install -g pnpm@10.33.4
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
# RUN cd /workspace/certd-server && \
# pnpm install --ignore-scripts && \
# yes | pnpm approve-builds && \
# pnpm rebuild && \
# npm run build-on-docker
FROM node:22-alpine
FROM node:22-alpine3.21
EXPOSE 7001
EXPOSE 7002
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
### Performance Improvements
* **设置:** 添加首页启用开关配置 ([25ad1e6](https://github.com/certd/certd/commit/25ad1e6f861e43288cc8bd90d4903628e6faec29))
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **domain:** 添加域名过期时间同步进度显示功能 ([9d2937d](https://github.com/certd/certd/commit/9d2937dd4b14ffab73e9b096edd2aa8539811182))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
+6 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.39.12",
"version": "1.39.13",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -12,7 +12,7 @@
"debug:force": "vite --force --mode debug",
"build": "cross-env NODE_OPTIONS=--max-old-space-size=40960 vite build ",
"dev-build": "echo 1",
"test:unit": "echo no unit tests",
"test:unit": "cross-env NODE_ENV=unittest echo no unit tests",
"test:vue": "vitest run",
"serve": "vite preview",
"preview": "vite preview",
@@ -62,7 +62,6 @@
"cos-js-sdk-v5": "^1.7.0",
"cron-parser": "^4.9.0",
"cropperjs": "^1.6.1",
"cross-env": "^7.0.3",
"cssnano": "^7.0.6",
"dayjs": "^1.11.7",
"defu": "^6.1.4",
@@ -107,8 +106,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/lib-iframe": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -127,6 +126,7 @@
"autoprefixer": "^10.4.21",
"caller-path": "^4.0.0",
"chai": "^5.1.0",
"cross-env": "^7.0.3",
"dependency-cruiser": "^16.2.3",
"dot": "^1.1.3",
"eslint": "8.57.0",
@@ -136,6 +136,7 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-vue": "^9.23.0",
"esmock": "^2.7.5",
"less": "^4.2.0",
"less-loader": "^12.2.0",
"postcss": "^8.4.35",
@@ -0,0 +1,106 @@
<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>
</div>
<div class="helper" :class="{ error: hasError }">
{{ message }}
</div>
</div>
</template>
<script setup lang="ts">
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { computed, inject, ref } from "vue";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RefreshInput",
});
type RefreshInputProps = ComponentPropsType & {
buttonText?: string;
icon?: string;
placeholder?: string;
successMessage?: string;
};
const fromType: any = inject("getFromType");
const getScope: any = inject("get:scope");
const getPluginType: any = inject("get:plugin:type", () => {
return "access";
});
const formItemContext = Form.useInjectFormItemContext();
const props = defineProps<RefreshInputProps>();
const emit = defineEmits<{
"update:value": [value: string];
}>();
const loading = ref(false);
const message = ref("");
const hasError = ref(false);
const action = computed(() => props.action);
const buttonText = computed(() => props.buttonText || "刷新");
const icon = computed(() => props.icon || "ion:refresh-outline");
const placeholder = computed(() => props.placeholder || "");
const successMessage = computed(() => props.successMessage || "刷新成功,请保存配置");
const doRefresh = async () => {
if (loading.value) {
return;
}
if (!action.value) {
hasError.value = true;
message.value = "缺少刷新动作配置";
return;
}
formItemContext.onFieldChange();
const { form } = getScope();
const pluginType = getPluginType();
const { input, record } = getInputFromForm(form, pluginType);
loading.value = true;
message.value = "";
hasError.value = false;
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: action.value,
input,
record,
fromType,
},
{
onError(err: any) {
hasError.value = true;
message.value = err.message;
},
showErrorNotify: false,
}
);
emit("update:value", res);
message.value = successMessage.value;
} finally {
loading.value = false;
}
};
</script>
<style lang="less" scoped>
.refresh-input-line {
display: flex;
gap: 8px;
align-items: center;
}
.refresh-input-control {
flex: 1;
}
</style>
@@ -12,6 +12,7 @@ import AccessSelector from "/@/views/certd/access/access-selector/index.vue";
import InputPassword from "./common/input-password.vue";
import CertInfoUpdater from "/@/views/certd/pipeline/cert-upload/index.vue";
import ApiTest from "./common/api-test.vue";
import RefreshInput from "./common/refresh-input.vue";
import ParamsShow from "./common/params-show.vue";
export * from "./cert/index.js";
export default {
@@ -23,6 +24,7 @@ export default {
app.component("CertInfoUpdater", CertInfoUpdater);
app.component("ApiTest", ApiTest);
app.component("RefreshInput", RefreshInput);
app.component("SynologyDeviceIdGetter", SynologyIdDeviceGetter);
app.component("RemoteAutoComplete", RemoteAutoComplete);
@@ -1,13 +1,16 @@
export default {
cnameTitle: "CNAME Service Configuration",
cnameDescription:
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
"The domain name configured here serves as a proxy for verifying other domains. When other domains apply for certificates, they map to this domain via CNAME for ownership verification. The advantage is that any domain can apply for a certificate this way without providing an AccessSecret.",
cnameLinkText: "CNAME principle and usage instructions",
cnameDomain: "CNAME Domain",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper:
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
"Requires a domain registered with a DNS provider on the right (or you can transfer other domain DNS servers here).\nOnce the CNAME domain is set, it cannot be changed. It is recommended to use a first-level subdomain.",
cnameDomainPattern: "Domain name cannot contain *",
cnameProviderSubdomain: "Delegated Subdomain",
cnameProviderSubdomainPlaceholder: "sub.example.com",
cnameProviderSubdomainHelper: "Fill this when the CNAME domain is hosted under a delegated subdomain, for example CNAME domain cname.sub.example.com and DNS zone sub.example.com.",
dnsProvider: "DNS Provider",
dnsProviderAuthorization: "DNS Provider Authorization",
};
@@ -6,6 +6,9 @@ export default {
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper: "需要一个右边DNS提供商注册的域名(也可以将其他域名的dns服务器转移到这几家来)。\nCNAME域名一旦确定不可修改,建议使用一级子域名",
cnameDomainPattern: "域名不能使用星号",
cnameProviderSubdomain: "托管子域名",
cnameProviderSubdomainPlaceholder: "sub.example.com",
cnameProviderSubdomainHelper: "当CNAME域名本身托管在子域名下时填写,例如 CNAME域名为 cname.sub.example.com,实际DNS托管域为 sub.example.com",
dnsProvider: "DNS提供商",
dnsProviderAuthorization: "DNS提供商授权",
};
@@ -23,6 +23,37 @@ export async function UpdateProfile(form: any) {
});
}
export async function GetContactCapability() {
return await request({
url: "/mine/contact/capability",
method: "POST",
});
}
export async function UpdateMobile(form: any) {
return await request({
url: "/mine/contact/mobile",
method: "POST",
data: form,
});
}
export async function VerifyContactIdentity(form: any) {
return await request({
url: "/mine/contact/verifyIdentity",
method: "POST",
data: form,
});
}
export async function UpdateEmail(form: any) {
return await request({
url: "/mine/contact/email",
method: "POST",
data: form,
});
}
export async function GetOauthBounds() {
return await request({
url: "/oauth/bounds",
@@ -0,0 +1,31 @@
import { defineComponent } from "vue";
import SmsCode from "/@/views/framework/login/sms-code.vue";
import EmailCode from "/@/views/framework/register/email-code.vue";
export const ContactCodeInput = defineComponent({
name: "ContactCodeInput",
props: {
modelValue: {
type: String,
default: "",
},
form: {
type: Object,
required: true,
},
type: {
type: String,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const onChange = (value: string) => emit("update:modelValue", value);
return () => {
if (props.type === "email") {
return <EmailCode value={props.modelValue} captcha={props.form.contactCaptcha} email={props.form.email} verificationType="bindEmail" onUpdate:value={onChange} />;
}
return <SmsCode value={props.modelValue} captcha={props.form.contactCaptcha} mobile={props.form.mobile} phoneCode={props.form.phoneCode} verificationType="bindMobile" onUpdate:value={onChange} />;
};
},
});
@@ -0,0 +1,33 @@
import { defineComponent } from "vue";
import SmsCode from "/@/views/framework/login/sms-code.vue";
import EmailCode from "/@/views/framework/register/email-code.vue";
export const IdentityCodeInput = defineComponent({
name: "IdentityCodeInput",
props: {
modelValue: {
type: String,
default: "",
},
form: {
type: Object,
required: true,
},
userInfo: {
type: Object,
required: true,
},
},
emits: ["update:modelValue"],
setup(props, { emit }) {
const onChange = (value: string) => emit("update:modelValue", value);
return () => {
if (props.form.identityType === "email") {
return <EmailCode value={props.modelValue} captcha={props.form.identityCaptcha} email={props.userInfo.email} verificationType="contactIdentity" onUpdate:value={onChange} />;
}
return (
<SmsCode value={props.modelValue} captcha={props.form.identityCaptcha} mobile={props.userInfo.mobile} phoneCode={props.userInfo.phoneCode || "86"} verificationType="contactIdentity" onUpdate:value={onChange} />
);
};
},
});
@@ -1,20 +1,22 @@
// useUserProfile, 获取 openEditProfileDialog ,参考 useTemplate方法
import { useFormWrapper } from "@fast-crud/fast-crud";
import { ref } from "vue";
import { cloneDeep, merge } from "lodash-es";
import { compute, dict } from "@fast-crud/fast-crud";
// 假设的 API 导入
import * as userProfileApi from "./api";
import { useUserStore } from "/@/store/user";
import { useI18n } from "/src/locales";
import CaptchaInput from "/@/components/captcha/captcha-input.vue";
import { message } from "ant-design-vue";
import { ContactCodeInput } from "./contact-code-input";
import { IdentityCodeInput } from "./identity-code-input";
import { useFormDialog } from "/@/use/use-dialog";
/**
*
* @returns {{openEditProfileDialog: openEditProfileDialog}}
*/
export function useUserProfile() {
const { openCrudFormDialog } = useFormWrapper();
const wrapperRef = ref();
const { openFormDialog } = useFormDialog();
async function openEditProfileDialog(req: { onUpdated?: (ctx: any) => void }) {
const detail = await userProfileApi.getMineInfo();
if (!detail) {
@@ -24,31 +26,28 @@ export function useUserProfile() {
const { t } = useI18n();
const userStore = useUserStore();
const userProfileFormRef = ref();
async function doSubmit(opts: { form: any }) {
const form = opts.form;
async function doSubmit(form: any) {
const { id } = await userProfileApi.UpdateProfile(form);
if (req.onUpdated) {
req.onUpdated({ id });
}
}
const crudOptions: any = {
form: {
doSubmit,
wrapper: {
title: `编辑用户资料`,
width: 1100,
onOpened(opts: { form: any }) {
merge(opts.form, detail);
},
},
await openFormDialog({
title: `编辑用户资料`,
wrapper: {
width: 600,
},
initialForm: detail,
onSubmit: doSubmit,
columns: {
nickName: {
title: t("certd.nickName"),
type: "text",
form: {
col: {
span: 24,
},
component: {
placeholder: t("certd.nickName"),
},
@@ -71,6 +70,9 @@ export function useUserProfile() {
},
},
form: {
col: {
span: 24,
},
component: {
vModel: "modelValue",
valueType: "key",
@@ -98,10 +100,7 @@ export function useUserProfile() {
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
});
}
return {
@@ -110,26 +109,20 @@ export function useUserProfile() {
}
export function usePasskeyRegister() {
const { openCrudFormDialog } = useFormWrapper();
const wrapperRef = ref();
const { openFormDialog } = useFormDialog();
async function openRegisterDialog(req: { onSubmit?: (ctx: any) => void }) {
const { t } = useI18n();
const userStore = useUserStore();
const deviceNameRef = ref();
const crudOptions: any = {
form: {
wrapper: {
title: t("authentication.registerPasskey"),
width: 500,
onOpened(opts: { form: any }) {
opts.form.deviceName = "";
},
},
onSubmit: req.onSubmit,
afterSubmit: null,
onSuccess: null,
await openFormDialog({
title: t("authentication.registerPasskey"),
wrapper: {
width: 500,
},
initialForm: {
deviceName: "",
},
onSubmit: async (form: any) => {
await req.onSubmit?.({ form });
},
columns: {
deviceName: {
@@ -147,15 +140,229 @@ export function usePasskeyRegister() {
},
},
},
};
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
return wrapper;
});
}
return {
openRegisterDialog,
};
}
export function useContactBind() {
const { openFormDialog } = useFormDialog();
async function openContactBindDialog(req: { type: "mobile" | "email"; userInfo: any; contactCapability: { smsEnabled?: boolean }; onUpdated?: () => Promise<void> | void }) {
const methods = [{ label: "密码", value: "password" }];
if (req.userInfo.email) {
methods.push({ label: "邮箱", value: "email" });
}
if (req.contactCapability.smsEnabled && req.userInfo.mobile) {
methods.push({ label: "手机号", value: "mobile" });
}
async function openChangeDialog(identityValidationCode: string) {
const isMobile = req.type === "mobile";
await openFormDialog({
title: isMobile ? (req.userInfo.mobile ? "修改手机号" : "绑定手机号") : req.userInfo.email ? "修改邮箱" : "绑定邮箱",
wrapper: {
width: 560,
},
initialForm: {
phoneCode: req.userInfo.phoneCode || "86",
mobile: req.userInfo.mobile || "",
email: req.userInfo.email || "",
contactCaptcha: null,
contactValidateCode: "",
},
async onSubmit(form: any) {
if (isMobile) {
await userProfileApi.UpdateMobile({
phoneCode: form.phoneCode,
mobile: form.mobile,
validateCode: form.contactValidateCode,
identityValidationCode,
});
} else {
await userProfileApi.UpdateEmail({
email: form.email,
validateCode: form.contactValidateCode,
identityValidationCode,
});
}
message.success("绑定信息已更新");
await req.onUpdated?.();
},
columns: {
phoneCode: {
title: "区号",
type: "text",
form: {
col: {
span: 24,
},
show: isMobile,
component: {
placeholder: "区号",
},
rules: [{ required: isMobile, message: "请输入区号" }],
},
},
mobile: {
title: "手机号",
type: "text",
form: {
col: {
span: 24,
},
show: isMobile,
component: {
placeholder: "请输入手机号",
},
rules: [
{ required: isMobile, message: "请输入手机号" },
{ pattern: /^\d{4,20}$/, message: "请输入正确的手机号" },
],
},
},
email: {
title: "邮箱",
type: "text",
form: {
col: {
span: 24,
},
show: !isMobile,
component: {
placeholder: "请输入邮箱",
},
rules: [
{ required: !isMobile, message: "请输入邮箱" },
{ type: "email", message: "请输入正确的邮箱" },
],
},
},
contactCaptcha: {
title: "图形验证码",
form: {
col: {
span: 24,
},
component: {
name: CaptchaInput,
vModel: "modelValue",
},
rules: [{ required: true, message: "请完成图形验证码" }],
},
},
contactValidateCode: {
title: isMobile ? "新手机号验证码" : "新邮箱验证码",
form: {
col: {
span: 24,
},
component: {
name: ContactCodeInput,
vModel: "modelValue",
form: compute(({ form }) => form),
type: req.type,
},
rules: [{ required: true, message: "请输入验证码" }],
},
},
},
});
}
await openFormDialog({
title: "验证本人操作",
wrapper: {
width: 520,
},
initialForm: {
identityType: "password",
identityPassword: "",
identityCaptcha: null,
identityValidateCode: "",
},
async onSubmit(form: any) {
const res = await userProfileApi.VerifyContactIdentity({
identityType: form.identityType,
identityPassword: form.identityPassword,
identityValidateCode: form.identityValidateCode,
});
await openChangeDialog(res.validationCode);
},
columns: {
identityType: {
title: "验证方式",
form: {
col: {
span: 24,
},
component: {
name: "fs-dict-radio",
vModel: "value",
dict: dict({
data: methods,
}),
},
rules: [{ required: true, message: "请选择验证方式" }],
valueChange({ form }: { form: any }) {
form.identityPassword = "";
form.identityCaptcha = null;
form.identityValidateCode = "";
},
},
},
identityPassword: {
title: "登录密码",
type: "password",
form: {
col: {
span: 24,
},
show: compute(({ form }) => form.identityType === "password"),
component: {
placeholder: "请输入登录密码",
},
rules: [{ required: true, message: "请输入登录密码" }],
},
},
identityCaptcha: {
title: "图形验证码",
form: {
col: {
span: 24,
},
show: compute(({ form }) => form.identityType !== "password"),
component: {
name: CaptchaInput,
vModel: "modelValue",
},
rules: [{ required: true, message: "请完成图形验证码" }],
},
},
identityValidateCode: {
title: "验证码",
form: {
col: {
span: 24,
},
show: compute(({ form }) => form.identityType !== "password"),
component: {
name: IdentityCodeInput,
vModel: "modelValue",
form: compute(({ form }) => form),
userInfo: req.userInfo,
},
rules: [{ required: true, message: "请输入验证码" }],
},
},
},
});
}
return {
openContactBindDialog,
};
}
@@ -13,32 +13,41 @@
<a-avatar v-else size="100" class="user-avatar default-avatar">
{{ userInfo.username }}
</a-avatar>
<a-button type="text" size="small" class="avatar-edit-btn" title="修改资料" @click.stop="doUpdate">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
<!-- <div class="status-indicator"></div> -->
</div>
<div class="user-info">
<h2 class="user-name flex items-center">
{{ userInfo.nickName }}
<fs-values-format :model-value="userInfo.roleIds" :dict="roleDict" color="blue" />
<span>{{ userInfo.nickName }}</span>
<a-button type="text" size="small" class="detail-edit-btn" title="修改资料" @click.stop="doUpdate">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
</h2>
<div class="user-details">
<a-tag color="blue" class="detail-tag">
<span class="tag-icon">👤</span>
{{ userInfo.username }}
<span class="tag-text">{{ userInfo.username }}</span>
<fs-values-format :model-value="userInfo.roleIds" type="text" :dict="roleDict" color="blue" />
</a-tag>
<a-tag v-if="userInfo.email" color="green" class="detail-tag">
<a-tag color="green" class="detail-tag">
<span class="tag-icon">📧</span>
{{ userInfo.email }}
<span class="tag-text">{{ userInfo.email || "未绑定邮箱" }}</span>
<a-button type="text" size="small" class="detail-edit-btn" title="修改邮箱" @click.stop="openBindContact('email')">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
</a-tag>
<a-tag v-if="userInfo.mobile" color="purple" class="detail-tag">
<a-tag v-if="contactCapability.smsEnabled" color="purple" class="detail-tag">
<span class="tag-icon">📱</span>
{{ userInfo.mobile }}
<span class="tag-text">{{ userInfo.mobile || "未绑定手机号" }}</span>
<a-button type="text" size="small" class="detail-edit-btn" title="修改手机号" @click.stop="openBindContact('mobile')">
<template #icon><fs-icon icon="ion:create-outline" /></template>
</a-button>
</a-tag>
</div>
</div>
<div class="action-buttons gap-2">
<a-button type="primary" class="action-btn" @click="doUpdate">
{{ t("authentication.updateProfile") }}
</a-button>
<change-password-button :show-button="true" />
<a-button type="primary" class="action-btn" @click="goSecuritySetting">
@@ -139,7 +148,7 @@ import * as api from "./api";
import { computed, onMounted, Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "/src/locales";
import { useUserProfile } from "./use";
import { useContactBind, useUserProfile } from "./use";
import { usePasskeyRegister } from "./use";
import { message, Modal, notification } from "ant-design-vue";
import { useSettingStore } from "/@/store/settings";
@@ -160,6 +169,9 @@ const settingStore = useSettingStore();
const userInfo: Ref = ref({});
const passkeys = ref([]);
const passkeySupported = ref(false);
const contactCapability = ref({
smsEnabled: false,
});
const getUserInfo = async () => {
userInfo.value = await api.getMineInfo();
@@ -177,6 +189,7 @@ function doUpdate() {
openEditProfileDialog({
onUpdated: async () => {
await getUserInfo();
userStore.setUserInfo(userInfo.value);
},
});
}
@@ -237,6 +250,23 @@ async function loadPasskeys() {
}
}
async function loadContactCapability() {
contactCapability.value = await api.GetContactCapability();
}
const { openContactBindDialog } = useContactBind();
async function openBindContact(type: "mobile" | "email") {
await openContactBindDialog({
type,
userInfo: userInfo.value,
contactCapability: contactCapability.value,
onUpdated: async () => {
await getUserInfo();
userStore.setUserInfo(userInfo.value);
},
});
}
async function unbindPasskey(id: number) {
Modal.confirm({
title: "确认解绑吗?",
@@ -366,6 +396,7 @@ const userAvatar = computed(() => {
onMounted(async () => {
await getUserInfo();
await loadContactCapability();
await loadOauthBounds();
await loadOauthProviders();
await loadPasskeys();
@@ -567,6 +598,24 @@ onMounted(async () => {
flex-shrink: 0;
}
.avatar-edit-btn {
position: absolute;
right: 2px;
bottom: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
min-width: 28px;
padding: 0;
color: #667eea;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 50%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.user-avatar {
border: 4px solid #ffffff;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
@@ -613,6 +662,18 @@ onMounted(async () => {
font-size: 13px;
}
.detail-edit-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
min-width: 20px;
margin: -2px -6px -2px 0;
padding: 0;
color: #667eea;
}
.tag-icon {
font-size: 14px;
}
@@ -863,18 +924,84 @@ onMounted(async () => {
}
@media (max-width: 768px) {
.card-header {
padding: 32px 16px;
}
.header-content {
flex-direction: column;
text-align: center;
gap: 18px;
}
.avatar-wrapper {
margin-bottom: 2px;
}
.user-avatar {
width: 88px !important;
height: 88px !important;
line-height: 88px !important;
}
.avatar-edit-btn {
right: -2px;
bottom: 0;
}
.user-name {
justify-content: center;
margin-bottom: 14px;
font-size: 22px;
gap: 6px;
}
.user-details {
justify-content: center;
flex-direction: column;
align-items: center;
gap: 6px;
width: 100%;
}
.detail-tag {
justify-content: flex-start;
width: min(230px, calc(100vw - 96px));
min-height: 34px;
padding: 5px 8px 5px 12px;
border-radius: 12px;
white-space: nowrap;
margin-right: 0;
text-align: left;
}
.tag-icon {
flex: 0 0 auto;
}
.tag-text {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.detail-edit-btn {
margin: 0 -4px 0 2px;
flex: 0 0 auto;
}
.action-buttons {
justify-content: center;
width: 100%;
gap: 10px;
margin-top: 2px;
}
.action-buttons :deep(.ant-btn) {
min-width: 90px;
height: 32px;
padding: 4px 12px;
}
}
}
@@ -97,6 +97,21 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
width: 200,
},
},
subdomain: {
title: t("certd.cnameProviderSubdomain"),
type: "text",
form: {
component: {
placeholder: t("certd.cnameProviderSubdomainPlaceholder"),
},
helper: t("certd.cnameProviderSubdomainHelper"),
rules: [{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") }],
},
column: {
width: 200,
show: false,
},
},
dnsProviderType: {
title: t("certd.dnsProvider"),
type: "dict-select",
+17
View File
@@ -3,6 +3,23 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.13](https://github.com/certd/certd/compare/v1.39.12...v1.39.13) (2026-05-10)
### Bug Fixes
* **aliyun-access:** 添加阿里云密钥校验失败的错误处理 ([b75c625](https://github.com/certd/certd/commit/b75c625ddcc0b3110699d8e6175681ef157b25df))
* cnameProvider域名支持设置子域名托管 ([7266af1](https://github.com/certd/certd/commit/7266af17491a98338022cfb18cfedfb93ca6ef8f))
* **plugin-aliyun:** 过滤非CAS证书并优化日志信息 ([c4b01da](https://github.com/certd/certd/commit/c4b01da384bc40a241a673ea8bc01ca733c04d83))
### Performance Improvements
* **用户资料:** 新增手机号邮箱绑定功能 ([e0eb0e2](https://github.com/certd/certd/commit/e0eb0e21f6dae24b639c944f9aba2c90496ab1c0))
* 域名注册过期时间获取再次优化 ([91a1b97](https://github.com/certd/certd/commit/91a1b9755066bf280e194dabf7c3a9f936e2643f))
* **证书流水线:** 添加批量更新证书申请参数功能 ([63be1c1](https://github.com/certd/certd/commit/63be1c1cbd9b09a3b48f26130c296b1cedcca1ac))
* 支持火山云vke ([bb46cb0](https://github.com/certd/certd/commit/bb46cb08f71f6ae921543f7e4a6c5f4e0190556e))
* 重构自动加载模块并优化EAB授权处理 ([4755216](https://github.com/certd/certd/commit/4755216505ad18555a50da9d8008c2207c48df86))
* **plugin-volcengine:** 支持火山引擎VKE部署插件 ([b8a64a6](https://github.com/certd/certd/commit/b8a64a6b5bf3691a47177de42bc49b798e795feb))
## [1.39.12](https://github.com/certd/certd/compare/v1.39.11...v1.39.12) (2026-04-29)
### Bug Fixes
@@ -0,0 +1 @@
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
@@ -0,0 +1 @@
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
@@ -0,0 +1 @@
ALTER TABLE cd_cname_provider ADD COLUMN subdomain varchar(100);
@@ -3,6 +3,26 @@ title: EAB授权
desc: ZeroSSL证书申请需要EAB授权
icon: ic:outline-lock
input:
eabType:
title: EAB类型
component:
name: a-select
options:
- value: google
label: Google
icon: flat-color-icons:google
- value: zerossl
label: ZeroSSL
icon: emojione:digit-zero
- value: litessl
label: litessl
icon: roentgen:free
- value: sslcom
label: SSL.com
icon: la:expeditedssl
helper: 请选择EAB类型
required: true
encrypt: false
kid:
title: KID
component:
@@ -24,8 +44,20 @@ input:
rules:
- type: email
message: 请输入正确的邮箱
helper: Google的EAB申请证书,更换邮箱会导致EAB失效,可以在此处绑定一个邮箱避免此问题
helper: 绑定一个邮箱避免失效
required: true
accountKey:
title: ACME账号私钥
component:
name: refresh-input
action: GenerateAccountKey
buttonText: 生成
successMessage: 账号私钥已生成,请保存授权配置
required: true
helper: |-
如果修改了KID,请点击生成重新生成账号私钥
注意:google的EAB只能生成一次账号私钥,更新私钥需要获取一个新的EAB授权
encrypt: true
pluginType: access
type: builtIn
scriptFilePath: /plugins/plugin-cert/access/eab-access.js
@@ -8,6 +8,7 @@ desc: 根据证书id一键更新腾讯云证书并自动部署(Id不变),
icon: svg:icon-tencentcloud
group: tencent
needPlus: false
deprecated: 腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口
input:
cert:
title: 域名证书
@@ -68,12 +68,12 @@ input:
component:
name: remote-select
vModel: value
mode: tags
mode: default
type: plugin
action: onGetClusterList
search: false
pager: false
multi: true
multi: false
watches:
- certDomains
- accessId
@@ -102,8 +102,6 @@ input:
value: Public
- label: 私网
value: Private
- label: 集群内
value: TargetCluster
value: Public
required: true
order: 0
+16 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.39.12",
"version": "1.39.13",
"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.39.12",
"@certd/basic": "^1.39.12",
"@certd/commercial-core": "^1.39.12",
"@certd/acme-client": "^1.39.13",
"@certd/basic": "^1.39.13",
"@certd/commercial-core": "^1.39.13",
"@certd/cv4pve-api-javascript": "^8.4.2",
"@certd/jdcloud": "^1.39.12",
"@certd/lib-huawei": "^1.39.12",
"@certd/lib-k8s": "^1.39.12",
"@certd/lib-server": "^1.39.12",
"@certd/midway-flyway-js": "^1.39.12",
"@certd/pipeline": "^1.39.12",
"@certd/plugin-cert": "^1.39.12",
"@certd/plugin-lib": "^1.39.12",
"@certd/plugin-plus": "^1.39.12",
"@certd/plus-core": "^1.39.12",
"@certd/jdcloud": "^1.39.13",
"@certd/lib-huawei": "^1.39.13",
"@certd/lib-k8s": "^1.39.13",
"@certd/lib-server": "^1.39.13",
"@certd/midway-flyway-js": "^1.39.13",
"@certd/pipeline": "^1.39.13",
"@certd/plugin-cert": "^1.39.13",
"@certd/plugin-lib": "^1.39.13",
"@certd/plugin-plus": "^1.39.13",
"@certd/plus-core": "^1.39.13",
"@google-cloud/dns": "^5.3.1",
"@google-cloud/publicca": "^1.3.0",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.185",
@@ -101,7 +101,6 @@
"cache-manager": "^6.1.0",
"cos-nodejs-sdk-v5": "^2.14.6",
"cron-parser": "^4.9.0",
"cross-env": "^7.0.3",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.7",
"esdk-obs-nodejs": "^3.25.6",
@@ -159,6 +158,8 @@
"@types/node": "^18",
"@types/nodemailer": "^6.4.8",
"c8": "^10.1.2",
"cross-env": "^7.0.3",
"esmock": "^2.7.5",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
@@ -1,9 +1,10 @@
import { BaseController, Constants } from '@certd/lib-server';
import { BaseController, Constants, SysSettingsService } from '@certd/lib-server';
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { PasskeyService } from '../../../modules/login/service/passkey-service.js';
import { RoleService } from '../../../modules/sys/authority/service/role-service.js';
import { UserService } from '../../../modules/sys/authority/service/user-service.js';
import { ApiTags } from '@midwayjs/swagger';
import { CodeService } from '../../../modules/basic/service/code-service.js';
/**
*/
@@ -20,8 +21,13 @@ export class MineController extends BaseController {
@Inject()
passkeyService: PasskeyService;
@Inject()
codeService: CodeService;
@Post('/info', { description: Constants.per.authOnly, summary: "查询用户信息" })
@Inject()
sysSettingsService: SysSettingsService;
@Post('/info', { description: Constants.per.authOnly, summary: '查询用户信息' })
public async info() {
const userId = this.getUserId();
const user = await this.userService.info(userId);
@@ -35,21 +41,75 @@ export class MineController extends BaseController {
return this.ok(user);
}
@Post('/changePassword', { description: Constants.per.authOnly, summary: "修改密码" })
@Post('/changePassword', { description: Constants.per.authOnly, summary: '修改密码' })
public async changePassword(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.userService.changePassword(userId, body);
return this.ok({});
}
@Post('/updateProfile', { description: Constants.per.authOnly, summary: "更新用户资料" })
@Post('/updateProfile', { description: Constants.per.authOnly, summary: '更新用户资料' })
public async updateProfile(@Body(ALL) body: any) {
const userId = this.getUserId();
await this.userService.updateProfile(userId, {
avatar: body.avatar,
nickName: body.nickName,
});
return this.ok({});
}
@Post('/contact/capability', { description: Constants.per.authOnly, summary: '查询联系方式绑定能力' })
public async contactCapability() {
const settings = await this.sysSettingsService.getPrivateSettings();
return this.ok({
smsEnabled: !!settings.sms?.config?.accessId,
});
}
@Post('/contact/verifyIdentity', { description: Constants.per.authOnly, summary: '验证本人操作' })
public async verifyContactIdentity(@Body(ALL) body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }) {
const userId = this.getUserId();
await this.userService.verifyIdentity(userId, body, this.codeService);
const validationCode = this.codeService.setValidationValue({
type: 'contactIdentity',
userId,
identityType: body.identityType,
});
return this.ok({ validationCode });
}
@Post('/contact/mobile', { description: Constants.per.authOnly, summary: '绑定或修改手机号' })
public async updateMobile(@Body(ALL) body: { phoneCode?: string; mobile: string; validateCode: string; identityValidationCode: string }) {
const userId = this.getUserId();
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
await this.codeService.checkSmsCode({
mobile: body.mobile,
phoneCode: body.phoneCode || '86',
smsCode: body.validateCode,
verificationType: 'bindMobile',
throwError: true,
});
await this.userService.updateMobile(userId, {
phoneCode: body.phoneCode,
mobile: body.mobile,
});
return this.ok({});
}
@Post('/contact/email', { description: Constants.per.authOnly, summary: '绑定或修改邮箱' })
public async updateEmail(@Body(ALL) body: { email: string; validateCode: string; identityValidationCode: string }) {
const userId = this.getUserId();
this.userService.checkContactIdentityValidation(userId, body.identityValidationCode, this.codeService);
this.codeService.checkEmailCode({
email: body.email,
validateCode: body.validateCode,
verificationType: 'bindEmail',
throwError: true,
});
await this.userService.updateEmail(userId, {
email: body.email,
});
return this.ok({});
}
}
@@ -1,7 +1,7 @@
import { logger } from '@certd/basic';
import { SysSettingsService, SysSiteInfo } from '@certd/lib-server';
import { getPlusInfo, isPlus } from "@certd/plus-core";
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import dayjs from "dayjs";
import { Between } from "typeorm";
import { DomainService } from '../cert/service/domain-service.js';
@@ -14,9 +14,9 @@ import { PipelineService } from '../pipeline/service/pipeline-service.js';
import { UserService } from "../sys/authority/service/user-service.js";
import { ProjectService } from '../sys/enterprise/service/project-service.js';
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoCRegisterCron {
export class AutoCron {
@Inject()
pipelineService: PipelineService;
@@ -53,7 +53,6 @@ export class AutoCRegisterCron {
@Init()
async init() {
logger.info('加载定时trigger开始');
await this.pipelineService.onStartup(this.immediateTriggerOnce, this.onlyAdminUser);
@@ -0,0 +1,182 @@
import assert from "assert";
import esmock from "esmock";
import { AutoFix, buildEabAccountKeyValue, buildLegacyGoogleAccountConfigWhere, parseStorageValue } from "./auto-fix.js";
function createAutoFix(options: { pluginConfigService?: any; accessService?: any; storageService?: any }) {
const autoFix = new AutoFix();
autoFix.pluginConfigService = options.pluginConfigService;
autoFix.accessService = options.accessService;
autoFix.storageService = options.storageService;
return autoFix;
}
describe("AutoFix", () => {
it("parses legacy storage values", () => {
const config = parseStorageValue(
JSON.stringify({
value: {
key: "legacy-private-key",
accountUrl: "https://example.com/acct/1",
},
})
);
assert.equal(config.key, "legacy-private-key");
});
it("builds the EAB account key payload", () => {
const payload = JSON.parse(buildEabAccountKeyValue("kid-1", "private-key"));
assert.deepEqual(payload, {
kid: "kid-1",
privateKey: "private-key",
});
});
it("builds legacy Google account config query by exact email key only", () => {
assert.deepEqual(buildLegacyGoogleAccountConfigWhere("user@example.com"), {
userId: 1,
scope: "user",
namespace: "1",
key: "acme.config.google.user@example.com",
});
});
it("finds legacy Google account config by exact email key only", async () => {
let findOneWhere: any;
let findCalled = false;
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: {
getRepository() {
return {
async findOne(options: any) {
findOneWhere = options.where;
return {
value: JSON.stringify({
value: {
privateKey: "legacy-private-key",
},
}),
};
},
async find() {
findCalled = true;
return [];
},
};
},
} as any,
});
const config = await autoFix.getLegacyGoogleAccountConfig("user@example.com");
assert.equal(config.privateKey, "legacy-private-key");
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
assert.equal(findCalled, false);
});
it("does not query legacy Google account config without email", async () => {
let repositoryCalled = false;
const autoFix = createAutoFix({
pluginConfigService: null as any,
accessService: null as any,
storageService: {
getRepository() {
repositoryCalled = true;
return {};
},
} as any,
});
const config = await autoFix.getLegacyGoogleAccountConfig();
assert.equal(config, null);
assert.equal(repositoryCalled, false);
});
it("skips Google common EAB account key fix outside commercial edition", async () => {
let pluginConfigCalled = false;
const autoFix = createAutoFix({
pluginConfigService: {
async getPluginConfig() {
pluginConfigCalled = true;
return null;
},
} as any,
accessService: null as any,
storageService: null as any,
});
await autoFix.init();
assert.equal(pluginConfigCalled, false);
});
it("fixes Google common EAB account key in commercial edition", async () => {
const { AutoFix: MockedAutoFix } = await esmock("./auto-fix.js", {
"@certd/plus-core": {
isComm: () => true,
},
});
let getAccessByIdArgs: any[] = [];
let findOneWhere: any;
let updateAccessParam: any;
const autoFix = new MockedAutoFix();
autoFix.pluginConfigService = {
async getPluginConfig(options: any) {
assert.deepEqual(options, {
name: "CertApply",
type: "builtIn",
});
return {
sysSetting: {
input: {
googleCommonEabAccessId: 12,
},
},
};
},
};
autoFix.accessService = {
async getAccessById(...args: any[]) {
getAccessByIdArgs = args;
return {
kid: "kid-1",
email: "user@example.com",
};
},
async updateAccess(param: any) {
updateAccessParam = param;
},
};
autoFix.storageService = {
getRepository() {
return {
async findOne(options: any) {
findOneWhere = options.where;
return {
value: JSON.stringify({
value: {
privateKey: "legacy-private-key",
},
}),
};
},
};
},
};
await autoFix.fixGoogleCommonEabAccountKey();
assert.deepEqual(getAccessByIdArgs, [12, false]);
assert.deepEqual(findOneWhere, buildLegacyGoogleAccountConfigWhere("user@example.com"));
assert.deepEqual(updateAccessParam, {
id: 12,
eabType: "google",
accountKey: buildEabAccountKeyValue("kid-1", "legacy-private-key"),
});
});
});
@@ -0,0 +1,107 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { AccessService } from "@certd/lib-server";
import { isComm } from "@certd/plus-core";
import { PluginConfigService } from "../plugin/service/plugin-config-service.js";
import { StorageService } from "../pipeline/service/storage-service.js";
export function parseStorageValue(value?: string) {
if (!value) {
return null;
}
try {
const parsed = JSON.parse(value);
return parsed?.value || null;
} catch {
return null;
}
}
export function buildEabAccountKeyValue(kid: string, privateKey: string) {
return JSON.stringify({
kid,
privateKey,
});
}
export function buildLegacyGoogleAccountConfigWhere(email: string) {
return {
userId: 1,
scope: "user",
namespace: "1",
key: `acme.config.google.${email}`,
};
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoFix {
@Inject()
pluginConfigService: PluginConfigService;
@Inject()
accessService: AccessService;
@Inject()
storageService: StorageService;
async init() {
await this.fixGoogleCommonEabAccountKey();
}
async fixGoogleCommonEabAccountKey() {
if (!isComm()) {
return;
}
try {
const certApplyConfig = await this.pluginConfigService.getPluginConfig({
name: "CertApply",
type: "builtIn",
});
const googleCommonEabAccessId = certApplyConfig?.sysSetting?.input?.googleCommonEabAccessId;
if (!googleCommonEabAccessId) {
return;
}
const eabAccess = await this.accessService.getAccessById(googleCommonEabAccessId, false);
if (eabAccess.accountKey) {
return;
}
if (!eabAccess.kid) {
logger.info("公共Google EAB授权缺少KID,跳过历史ACME账号私钥修复");
return;
}
const accountConfig = await this.getLegacyGoogleAccountConfig(eabAccess.email);
const privateKey = accountConfig?.privateKey || accountConfig?.key || accountConfig?.accountKey;
if (!privateKey) {
logger.info("未找到可迁移到公共Google EAB授权的历史ACME账号私钥");
return;
}
const accountKey = buildEabAccountKeyValue(eabAccess.kid, privateKey);
await this.accessService.updateAccess({ id: googleCommonEabAccessId, eabType: "google", accountKey });
logger.info(`已修复公共Google EAB授权的ACME账号私钥,accessId=${googleCommonEabAccessId}`);
} catch (e: any) {
logger.error("修复公共Google EAB授权ACME账号私钥失败", e);
}
}
async getLegacyGoogleAccountConfig(email?: string) {
if (!email) {
return null;
}
const repository = this.storageService.getRepository();
const exact = await repository.findOne({
where: buildLegacyGoogleAccountConfigWhere(email),
});
const exactValue = this.parseStorageValue(exact?.value);
if (exactValue?.key || exactValue?.privateKey || exactValue?.accountKey) {
return exactValue;
}
return null;
}
parseStorageValue(value?: string) {
return parseStorageValue(value);
}
}
@@ -1,14 +1,14 @@
import { logger } from '@certd/basic';
import { PlusService, SysInstallInfo, SysPrivateSettings, SysSettingsService } from '@certd/lib-server';
import { Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import crypto from 'crypto';
import { nanoid } from 'nanoid';
import { UserService } from '../sys/authority/service/user-service.js';
import { SafeService } from "../sys/settings/safe-service.js";
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoAInitSite {
export class AutoInitSite {
@Inject()
userService: UserService;
@@ -22,7 +22,6 @@ export class AutoAInitSite {
@Inject()
safeService: SafeService;
@Init()
async init() {
logger.info('初始化站点开始');
await this.startOptimizeDb();
@@ -1,16 +1,15 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { logger } from "@certd/basic";
import { PluginService } from "../plugin/service/plugin-service.js";
import { registerPaymentProviders } from "../suite/payments/index.js";
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoBLoadPlugins {
export class AutoLoadPlugins {
@Inject()
pluginService: PluginService;
@Init()
async init() {
logger.info(`加载插件开始,加载模式:${process.env.certd_plugin_loadmode}`);
if (process.env.certd_plugin_loadmode === "metadata") {
@@ -1,14 +1,13 @@
import { logger, utils } from '@certd/basic';
import { UserSuiteService } from '@certd/commercial-core';
import { Autoload, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoDMitterRegister {
export class AutoMitterRegister {
@Inject()
userSuiteService: UserSuiteService;
@Init()
async init() {
await this.registerOnNewUser();
}
@@ -1,21 +1,21 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { CertInfoService } from "../monitor/index.js";
import { pipelineEmitter } from "@certd/pipeline";
import { CertInfo, EVENT_CERT_APPLY_SUCCESS } from "@certd/plugin-cert";
import { PipelineEvent } from "@certd/pipeline";
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoEPipelineEmitterRegister {
export class AutoPipelineEmitterRegister {
@Inject()
certInfoService: CertInfoService;
@Init()
async init() {
await this.onCertApplySuccess();
}
async onCertApplySuccess() {
pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{cert:CertInfo,file:string}>) => {
pipelineEmitter.on(EVENT_CERT_APPLY_SUCCESS, async (event: PipelineEvent<{ cert: CertInfo; file: string }>) => {
await this.certInfoService.updateCertByPipelineId(event.pipeline.id, event.event.cert, event.event.file);
});
}
@@ -1,4 +1,4 @@
import { App, Autoload, Config, Init, Inject, Scope, ScopeEnum } from '@midwayjs/core';
import { App, Config, Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { getPlusInfo, isPlus } from '@certd/plus-core';
import { isDev, logger } from '@certd/basic';
@@ -11,9 +11,9 @@ import { UserService } from '../sys/authority/service/user-service.js';
import { UserSettingsService } from '../mine/service/user-settings-service.js';
import { startProxyServer } from './proxy/server.js';
@Autoload()
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoZPrint {
export class AutoPrint {
@Inject()
sysSettingsService: SysSettingsService;
@@ -34,7 +34,6 @@ export class AutoZPrint {
@Config('system.resetAdminPasswd')
private resetAdminPasswd: boolean;
@Init()
async init() {
//监听https
this.startHttpsServer();
@@ -0,0 +1,44 @@
import { Autoload, Init, Inject, Scope, ScopeEnum } from "@midwayjs/core";
import { AutoInitSite } from "./auto-init-site.js";
import { AutoLoadPlugins } from "./auto-load-plugins.js";
import { AutoCron } from "./auto-cron.js";
import { AutoMitterRegister } from "./auto-mitter-register.js";
import { AutoPipelineEmitterRegister } from "./auto-pipeline-emitter-register.js";
import { AutoFix } from "./auto-fix.js";
import { AutoPrint } from "./auto-print.js";
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class AutoRegister {
@Inject()
autoInitSite: AutoInitSite;
@Inject()
autoLoadPlugins: AutoLoadPlugins;
@Inject()
autoCron: AutoCron;
@Inject()
autoMitterRegister: AutoMitterRegister;
@Inject()
autoPipelineEmitterRegister: AutoPipelineEmitterRegister;
@Inject()
autoPrint: AutoPrint;
@Inject()
autoFix: AutoFix;
@Init()
async init() {
await this.autoInitSite.init();
await this.autoLoadPlugins.init();
await this.autoCron.init();
await this.autoMitterRegister.init();
await this.autoPipelineEmitterRegister.init();
await this.autoFix.init();
await this.autoPrint.init();
}
}
@@ -11,6 +11,8 @@ export class CnameProviderEntity {
userId: number;
@Column({ comment: '域名', length: 100 })
domain: string;
@Column({ comment: '子域名托管', length: 100, nullable: true })
subdomain: string;
@Column({ comment: 'DNS提供商类型', name: 'dns_provider_type', length: 20 })
dnsProviderType: string;
@Column({ comment: 'DNS授权Id', name: 'access_id' })
@@ -98,6 +98,18 @@ export class CnameProviderService extends BaseService<CnameProviderEntity> {
return null;
}
async getSubDomains() {
const list = await this.repository.find({
select: {
subdomain: true,
},
where: {
disabled: false,
},
});
return list.map(item => item.subdomain?.trim()).filter((item): item is string => !!item);
}
async list(req: ListReq): Promise<any[]> {
const list = await super.list(req);
const sysPrivateSettings = await this.settingsService.getSetting<SysPrivateSettings>(SysPrivateSettings);
@@ -0,0 +1,28 @@
import assert from "node:assert/strict";
import { describe, it } from "mocha";
import { SubDomainsGetter } from "./sub-domain-getter.js";
describe("SubDomainsGetter", () => {
it("returns subdomains configured on system cname providers", async () => {
const subDomainService = {
async getListByUserId() {
return ["example.com"];
},
} as any;
const domainService = {
async findOne() {
return null;
},
} as any;
const cnameProviderService = {
async getSubDomains() {
return ["cname-hosted.example.com"];
},
} as any;
const getter = new SubDomainsGetter(1, 2, subDomainService, domainService, cnameProviderService);
assert.deepEqual(await getter.getSubDomains(), ["cname-hosted.example.com", "example.com"]);
assert.equal(await getter.hasSubDomain("txt.certd.cname-hosted.example.com"), "cname-hosted.example.com");
});
});
@@ -1,22 +1,30 @@
import { ISubDomainsGetter } from "@certd/plugin-cert";
import { SubDomainService } from "../sub-domain-service.js";
import { DomainService } from "../../../cert/service/domain-service.js";
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
projectId: number;
subDomainService: SubDomainService;
domainService: DomainService;
cnameProviderService: CnameProviderService;
constructor(userId: number, projectId: number, subDomainService: SubDomainService, domainService: DomainService) {
constructor(userId: number, projectId: number, subDomainService: SubDomainService, domainService: DomainService, cnameProviderService: CnameProviderService) {
this.userId = userId;
this.projectId = projectId;
this.subDomainService = subDomainService;
this.domainService = domainService;
this.cnameProviderService = cnameProviderService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId, this.projectId)
const projectSubDomains = await this.subDomainService.getListByUserId(this.userId, this.projectId) || [];
const cnameProviderSubDomains = await this.cnameProviderService.getSubDomains();
return [...projectSubDomains, ...cnameProviderSubDomains]
.map(item => item?.trim())
.filter((item): item is string => !!item)
.sort((a, b) => b.length - a.length);
}
async hasSubDomain(fullDomain: string) {
@@ -12,6 +12,7 @@ import { SubDomainService } from "../sub-domain-service.js";
import { CertInfoGetter } from "./cert-info-getter.js";
import { CertInfoService } from "../../../monitor/index.js";
import { ICertInfoGetter } from "@certd/plugin-lib";
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
const serviceNames = [
'ocrService',
@@ -53,7 +54,8 @@ export class TaskServiceGetter implements IServiceGetter{
async getSubDomainsGetter(): Promise<SubDomainsGetter> {
const subDomainsService:SubDomainService = await this.appCtx.getAsync("subDomainService")
const domainService:DomainService = await this.appCtx.getAsync("domainService")
return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService)
const cnameProviderService:CnameProviderService = await this.appCtx.getAsync("cnameProviderService")
return new SubDomainsGetter(this.userId,this.projectId, subDomainsService,domainService,cnameProviderService)
}
async getCertInfoGetter(): Promise<ICertInfoGetter> {
@@ -102,4 +104,3 @@ export type TaskServiceCreateReq = {
@@ -0,0 +1,27 @@
/// <reference types="mocha" />
import assert from 'node:assert/strict';
import { Not } from 'typeorm';
import { buildUserContactConflictWhere } from './user-service.js';
describe('buildUserContactConflictWhere', () => {
it('checks username, mobile and email conflicts except current user', () => {
const where = buildUserContactConflictWhere('user@example.com', 12);
assert.deepEqual(where, [
{ username: 'user@example.com', id: Not(12) },
{ mobile: 'user@example.com', id: Not(12) },
{ email: 'user@example.com', id: Not(12) },
]);
});
it('trims contact value before building conflict query', () => {
const where = buildUserContactConflictWhere(' 13800138000 ', 3);
assert.deepEqual(where, [
{ username: '13800138000', id: Not(3) },
{ mobile: '13800138000', id: Not(3) },
{ email: '13800138000', id: Not(3) },
]);
});
});
@@ -1,6 +1,6 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import {EntityManager, In, MoreThan, Not, Repository} from 'typeorm';
import { EntityManager, In, MoreThan, Not, Repository } from 'typeorm';
import { UserEntity } from '../entity/user.js';
import * as _ from 'lodash-es';
import { BaseService, CommonException, Constants, FileService, SysInstallInfo, SysSettingsService } from '@certd/lib-server';
@@ -18,15 +18,22 @@ import { OauthBoundService } from '../../../login/service/oauth-bound-service.js
export type RegisterType = 'username' | 'mobile' | 'email';
export type ForgotPasswordType = 'mobile' | 'email';
export const AdminRoleId = 1
export const AdminRoleId = 1;
export function buildUserContactConflictWhere(value: string, userId: number) {
const contact = value?.trim();
return [
{ username: contact, id: Not(userId) },
{ mobile: contact, id: Not(userId) },
{ email: contact, id: Not(userId) },
];
}
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class UserService extends BaseService<UserEntity> {
@InjectEntityModel(UserEntity)
repository: Repository<UserEntity>;
@Inject()
@@ -44,10 +51,9 @@ export class UserService extends BaseService<UserEntity> {
@Inject()
dbAdapter: DbAdapter;
@Inject()
@Inject()
oauthBoundService: OauthBoundService;
//@ts-ignore
getRepository() {
return this.repository;
@@ -145,7 +151,7 @@ export class UserService extends BaseService<UserEntity> {
return bcrypt.hashSync(plainPassword, salt);
}
async findOne(param: Record<string,any>) {
async findOne(param: Record<string, any>) {
return this.repository.findOne({
where: param,
});
@@ -177,12 +183,11 @@ export class UserService extends BaseService<UserEntity> {
return await this.roleService.getPermissionByRoleIds(roleIds);
}
async register(type: string, user: UserEntity,withTx?:(tx: EntityManager)=>Promise<void>) {
async register(type: string, user: UserEntity, withTx?: (tx: EntityManager) => Promise<void>) {
if (!user.password) {
user.password = simpleNanoId();
}
if (user.username) {
const username = user.username;
const old = await this.findOne([{ username: username }, { mobile: username }, { email: username }]);
@@ -208,7 +213,6 @@ export class UserService extends BaseService<UserEntity> {
}
}
if (!user.username) {
user.username = 'user_' + simpleNanoId();
}
@@ -235,7 +239,7 @@ export class UserService extends BaseService<UserEntity> {
const userRole: UserRoleEntity = UserRoleEntity.of(newUser.id, Constants.role.defaultUser);
await txManager.save(userRole);
if(withTx) {
if (withTx) {
await withTx(txManager);
}
});
@@ -247,35 +251,26 @@ export class UserService extends BaseService<UserEntity> {
return newUser;
}
async forgotPassword(
data: {
type: ForgotPasswordType;
input?: string,
phoneCode?: string,
validateCode: string,
password: string,
confirmPassword: string,
}
) {
if(!data.type) {
async forgotPassword(data: { type: ForgotPasswordType; input?: string; phoneCode?: string; validateCode: string; password: string; confirmPassword: string }) {
if (!data.type) {
throw new CommonException('找回类型不能为空');
}
if(data.password !== data.confirmPassword) {
if (data.password !== data.confirmPassword) {
throw new CommonException('两次输入的密码不一致');
}
const where :any= {
const where: any = {
[data.type]: data.input,
};
if (data.type === 'mobile' ) {
where.phoneCode = data.phoneCode ?? '86';
if (data.type === 'mobile') {
where.phoneCode = data.phoneCode ?? '86';
}
const user = await this.findOne({ [data.type]: data.input });
console.log('user', user)
if(!user) {
console.log('user', user);
if (!user) {
throw new CommonException('用户不存在');
// return;
}
await this.resetPassword(user.id, data.password)
await this.resetPassword(user.id, data.password);
return user.username;
}
@@ -376,30 +371,102 @@ export class UserService extends BaseService<UserEntity> {
}
async getAdmins() {
const admins = await this.userRoleService.find({
where: {
roleId: AdminRoleId,
},
});
const admins = await this.userRoleService.find({
where: {
roleId: AdminRoleId,
},
});
const userIds = admins.map(item => item.userId);
return await this.repository.find({
where: {
id: In(userIds),
status: 1,
},
order: {
updateTime: 'DESC',
},
})
const userIds = admins.map(item => item.userId);
return await this.repository.find({
where: {
id: In(userIds),
status: 1,
},
order: {
updateTime: 'DESC',
},
});
}
async updateProfile(userId: any, body: any) {
await this.update({
id: userId,
...body,
})
});
}
async verifyIdentity(userId: number, body: { identityType: 'password' | 'email' | 'mobile'; identityPassword?: string; identityValidateCode?: string }, codeService: any) {
const user = await this.info(userId);
if (body.identityType === 'password') {
const passwordChecked = await this.checkPassword(body.identityPassword, user.password, user.passwordVersion);
if (!passwordChecked) {
throw new CommonException('密码错误');
}
return;
}
if (body.identityType === 'email') {
if (!user.email) {
throw new CommonException('当前账号未绑定邮箱');
}
codeService.checkEmailCode({
email: user.email,
validateCode: body.identityValidateCode,
verificationType: 'contactIdentity',
throwError: true,
});
return;
}
if (body.identityType === 'mobile') {
if (!user.mobile) {
throw new CommonException('当前账号未绑定手机号');
}
await codeService.checkSmsCode({
mobile: user.mobile,
phoneCode: user.phoneCode || '86',
smsCode: body.identityValidateCode,
verificationType: 'contactIdentity',
throwError: true,
});
return;
}
throw new CommonException('不支持的验证方式');
}
checkContactIdentityValidation(userId: number, validationCode: string, codeService: any) {
const validationValue = codeService.getValidationValue(validationCode);
if (!validationValue || validationValue.type !== 'contactIdentity' || validationValue.userId !== userId) {
throw new CommonException('请先验证本人操作');
}
}
async updateMobile(userId: number, body: { phoneCode?: string; mobile: string }) {
const mobile = body.mobile?.trim();
if (!mobile) {
throw new CommonException('手机号不能为空');
}
const old = await this.findOne(buildUserContactConflictWhere(mobile, userId));
if (old != null) {
throw new CommonException('手机号已被占用');
}
await this.repository.update(userId, {
phoneCode: body.phoneCode || '86',
mobile,
});
}
async updateEmail(userId: number, body: { email: string }) {
const email = body.email?.trim();
if (!email) {
throw new CommonException('邮箱不能为空');
}
const old = await this.findOne(buildUserContactConflictWhere(email, userId));
if (old != null) {
throw new CommonException('邮箱已被占用');
}
await this.repository.update(userId, {
email,
});
}
async getAllUserIds() {
@@ -408,7 +475,7 @@ export class UserService extends BaseService<UserEntity> {
where: {
status: 1,
},
})
});
return users.map(item => item.id);
}
}
@@ -275,11 +275,13 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
}
});
const list = certListRes.Result;
let list = certListRes.Result || [];
list = list.filter((item: any) => item.Type === "cas");
if (!list || list.length === 0) {
this.logger.info(`站点[${siteId}]没有证书, 无需删除`);
this.logger.info(`站点[${siteId}]没有CAS证书, 无需删除`);
return
}
if (list.length < certLimit) {
this.logger.info(`站点[${siteId}]证书数量(${list.length})未超限制, 无需删除`);
return;
@@ -0,0 +1,20 @@
import assert from "assert";
import { EabAccess } from "./eab-access.js";
describe("EabAccess", () => {
it("generates an account key payload for the current kid", async () => {
const access = new EabAccess();
access.kid = "kid-1";
const payload = JSON.parse(await access.onGenerateAccountKey());
assert.equal(payload.kid, "kid-1");
assert.match(payload.privateKey, /BEGIN (RSA )?PRIVATE KEY/);
});
it("requires kid before generating the account key payload", async () => {
const access = new EabAccess();
await assert.rejects(() => access.onGenerateAccountKey(), /请先填写KID/);
});
});
@@ -1,4 +1,5 @@
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
import * as acme from "@certd/acme-client";
@IsAccess({
name: "eab",
@@ -7,6 +8,23 @@ import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
icon: "ic:outline-lock",
})
export class EabAccess extends BaseAccess {
@AccessInput({
title: "EAB类型",
component: {
name: "a-select",
options: [
{ value: "google", label: "Google", icon: "flat-color-icons:google" },
{ value: "zerossl", label: "ZeroSSL", icon: "emojione:digit-zero" },
{ value: "litessl", label: "litessl", icon: "roentgen:free" },
{ value: "sslcom", label: "SSL.com", icon: "la:expeditedssl" },
],
},
helper: "请选择EAB类型",
required: true,
encrypt: false,
})
eabType = "";
@AccessInput({
title: "KID",
component: {
@@ -34,10 +52,35 @@ export class EabAccess extends BaseAccess {
placeholder: "绑定一个邮箱",
},
rules: [{ type: "email", message: "请输入正确的邮箱" }],
helper: "Google的EAB申请证书,更换邮箱会导致EAB失效,可以在此处绑定一个邮箱避免此问题",
helper: "绑定一个邮箱避免失效",
required: true,
})
email = "";
@AccessInput({
title: "ACME账号私钥",
component: {
name: "refresh-input",
action: "GenerateAccountKey",
buttonText: "生成",
successMessage: "账号私钥已生成,请保存授权配置",
},
required: true,
helper: "如果修改了KID,请点击生成重新生成账号私钥\n注意:google的EAB只能生成一次账号私钥,更新私钥需要获取一个新的EAB授权",
encrypt: true,
})
accountKey = "";
async onGenerateAccountKey() {
if (!this.kid) {
throw new Error("请先填写KID");
}
const key = await acme.crypto.createPrivateKey(2048);
return JSON.stringify({
kid: this.kid,
privateKey: key.toString(),
});
}
}
new EabAccess();
@@ -0,0 +1,176 @@
import assert from "assert";
import { AcmeService } from "./acme.js";
const logger = {
info() {},
error() {},
warn() {},
debug() {},
};
describe("AcmeService account config", () => {
it("keeps legacy email-based account config when EAB has no saved account key", async () => {
const userContext = {
async getObj(key: string) {
if (key === "acme.config.google.user@example.com") {
return {
key: "legacy-email-key",
accountUrl: "https://dv.acme-v02.api.pki.goog/acme/acct/legacy",
};
}
return null;
},
async setObj() {},
};
const service = new AcmeService({
userId: 1,
userContext: userContext as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-1",
hmacKey: "hmac",
} as any,
domainParser: {} as any,
});
const conf = await service.getAccountConfig("user@example.com", { enabled: false, mappings: {} });
assert.equal(conf.key, "legacy-email-key");
assert.equal(conf.accountUrl, "https://dv.acme-v02.api.pki.goog/acme/acct/legacy");
});
it("uses the account key saved on the EAB access before legacy email config", async () => {
const userContext = {
async getObj(key: string) {
if (key === "acme.config.google.access.12") {
return { accountUrl: "https://dv.acme-v02.api.pki.goog/acme/acct/1" };
}
if (key === "acme.config.google.user@example.com") {
return { key: "legacy-email-key" };
}
return null;
},
async setObj() {},
};
const service = new AcmeService({
userId: 1,
userContext: userContext as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-1",
hmacKey: "hmac",
accountKey: JSON.stringify({ kid: "kid-1", privateKey: "eab-account-key" }),
} as any,
domainParser: {} as any,
});
const conf = await service.getAccountConfig("user@example.com", { enabled: false, mappings: {} });
assert.equal(conf.key, "eab-account-key");
assert.equal(conf.accountUrl, "https://dv.acme-v02.api.pki.goog/acme/acct/1");
});
it("rejects an EAB account key generated for another kid", async () => {
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-2",
hmacKey: "hmac",
accountKey: JSON.stringify({ kid: "kid-1", privateKey: "eab-account-key" }),
} as any,
domainParser: {} as any,
});
assert.throws(() => service.getEabAccountPrivateKey(), /请点击刷新重新生成ACME账号私钥/);
});
it("formats expired EAB errors with a Chinese recovery hint", () => {
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "google",
eab: {
id: 12,
kid: "kid-1",
hmacKey: "hmac",
} as any,
domainParser: {} as any,
});
const error = service.formatCreateAccountError(new Error("Unknown external account binding (EAB) key. This may be due to the EAB key expiring"));
assert.match(error.message, /EAB授权已失效或已过期/);
assert.match(error.message, /请重新获取EAB授权并刷新ACME账号私钥后重试/);
});
});
describe("AcmeService challenge", () => {
it("parses cname TXT full record to choose the delegated DNS zone", async () => {
const parseCalls: string[] = [];
const service = new AcmeService({
userId: 1,
userContext: {} as any,
logger: logger as any,
sslProvider: "letsencrypt",
domainParser: {
async parse(fullDomain: string) {
parseCalls.push(fullDomain);
if (fullDomain === "certd-key.cname.sub.example.com") {
return "sub.example.com";
}
return "example.com";
},
} as any,
});
const dnsProvider = {
usePunyCode() {
return false;
},
async createRecord(recordReq: any) {
assert.equal(recordReq.domain, "sub.example.com");
assert.equal(recordReq.fullRecord, "certd-key.cname.sub.example.com");
assert.equal(recordReq.hostRecord, "certd-key.cname");
return { id: "record-id" };
},
} as any;
await service.challengeCreateFn(
{
identifier: {
value: "www.example.com",
},
challenges: [
{
type: "dns-01",
},
],
},
async () => "key-auth",
{
domainsVerifyPlan: {
"www.example.com": {
type: "cname",
domain: "www.example.com",
mainDomain: "example.com",
cnameVerifyPlan: {
domain: "cname.sub.example.com",
fullRecord: "certd-key.cname.sub.example.com",
dnsProvider,
},
},
},
}
);
assert.deepEqual(parseCalls, ["www.example.com", "certd-key.cname.sub.example.com"]);
});
});
@@ -49,11 +49,15 @@ export type CertInfo = {
};
export type SSLProvider = "letsencrypt" | "google" | "zerossl" | "sslcom" | "letsencrypt_staging";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
type AcmeEabOptions = ClientExternalAccountBindingOptions & {
id?: number;
accountKey?: string;
};
type AcmeServiceOptions = {
userContext: IContext;
logger: ILogger;
sslProvider: SSLProvider;
eab?: ClientExternalAccountBindingOptions;
eab?: AcmeEabOptions;
skipLocalVerify?: boolean;
useMappingProxy?: boolean;
reverseProxy?: string;
@@ -71,7 +75,7 @@ export class AcmeService {
logger: ILogger;
sslProvider: SSLProvider;
skipLocalVerify = true;
eab?: ClientExternalAccountBindingOptions;
eab?: AcmeEabOptions;
constructor(options: AcmeServiceOptions) {
this.options = options;
this.userContext = options.userContext;
@@ -85,7 +89,14 @@ export class AcmeService {
}
async getAccountConfig(email: string, urlMapping: UrlMapping): Promise<any> {
const conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {};
let conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {};
const eabAccountKey = this.getEabAccountPrivateKey();
if (eabAccountKey) {
conf = {
...((await this.userContext.getObj(this.buildAccessAccountKey())) || {}),
key: eabAccountKey,
};
}
if (urlMapping && urlMapping.mappings) {
for (const key in urlMapping.mappings) {
if (Object.prototype.hasOwnProperty.call(urlMapping.mappings, key)) {
@@ -104,16 +115,49 @@ export class AcmeService {
return `acme.config.${this.sslProvider}.${email}`;
}
buildAccessAccountKey() {
return `acme.config.${this.sslProvider}.access.${this.eab.id}`;
}
getEabAccountPrivateKey() {
if (!this.eab?.accountKey) {
return null;
}
let accountKey;
try {
accountKey = JSON.parse(this.eab.accountKey);
} catch {
return this.eab.accountKey;
}
if (accountKey.kid !== this.eab.kid) {
throw new Error("EAB的KID已变化,请点击刷新重新生成ACME账号私钥");
}
return accountKey.privateKey;
}
formatCreateAccountError(e: any) {
const message = e?.message || "";
if (message.includes("Unknown external account binding (EAB) key")) {
return new Error(`EAB授权已失效或已过期,请重新获取EAB授权并刷新ACME账号私钥后重试。原始错误:${message}`);
}
return e;
}
async saveAccountConfig(email: string, conf: any) {
if (this.getEabAccountPrivateKey()) {
// userContext 跟用户走。公共 EAB 场景下这里仅作为当前用户缓存;
// 其他用户会通过 onlyReturnExisting 用同一个账号私钥取回 accountUrl。
await this.userContext.setObj(this.buildAccessAccountKey(), { accountUrl: conf.accountUrl });
return;
}
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 });
let targetUrl = directoryUrl.replace("https://", "");
targetUrl = targetUrl.substring(0, targetUrl.indexOf("/"));
const mappings = {
"acme-v02.api.letsencrypt.org": "le.px.certd.handfree.work",
"dv.acme-v02.api.pki.goog": "gg.px.certd.handfree.work",
@@ -171,7 +215,23 @@ export class AcmeService {
contact: [`mailto:${email}`],
externalAccountBinding: this.eab,
};
await client.createAccount(accountPayload);
if (this.getEabAccountPrivateKey()) {
try {
// RFC 8555 的 newAccount 支持 onlyReturnExisting。
// 使用同一个账号私钥时,CA 会返回已存在账号的 URL,不会再次消费 EAB。
await client.createAccount({ onlyReturnExisting: true });
conf.accountUrl = client.getAccountUrl();
await this.saveAccountConfig(email, conf);
return client;
} catch (e: any) {
this.logger.info(`未找到已存在的ACME账号,准备创建新账号:${e.message}`);
}
}
try {
await client.createAccount(accountPayload);
} catch (e: any) {
throw this.formatCreateAccountError(e);
}
conf.accountUrl = client.getAccountUrl();
await this.saveAccountConfig(email, conf);
}
@@ -264,8 +324,8 @@ export class AcmeService {
const cname: CnameVerifyPlan = domainVerifyPlan.cnameVerifyPlan;
if (cname) {
dnsProvider = cname.dnsProvider;
domain = await this.options.domainParser.parse(cname.domain);
fullRecord = cname.fullRecord;
domain = await this.options.domainParser.parse(fullRecord);
} else {
this.logger.error(`未找到域名${fullDomain}的CNAME校验计划,请修改证书申请配置`);
}
@@ -21,6 +21,7 @@ import { omit } from "lodash-es";
//插件分组
group: pluginGroups.tencent.key,
needPlus: false,
deprecated: "腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口",
default: {
//默认值配置照抄即可
strategy: {
+1
View File
@@ -23,3 +23,4 @@
},
"exclude": ["*.js", "*.ts", "dist", "node_modules", "src/**/*.test.ts", "test"]
}