Compare commits

..
20 Commits
Author SHA1 Message Date
xiaojunnuo e9921928df perf: 部署到京东云CDN支持设置跳转类型 2026-08-07 09:15:00 +08:00
xiaojunnuo fa0c87dec3 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-08-06 12:00:18 +08:00
xiaojunnuo ba64c2d147 prf: 新增阿里云ACK部署调试支持 2026-08-06 12:00:11 +08:00
xiaojunnuo cf008b9cff fix: 优化火山引擎dcdn,单个单个部署,规避超时问题 2026-08-06 11:57:40 +08:00
xiaojunnuo 9d6f7ef0f2 style: 统一代码中单引号为双引号并补全格式 2026-08-02 21:31:30 +08:00
xiaojunnuo 0ddb1f69d2 perf(cert-plugin): 证书申请支持失败重试 2026-08-02 21:07:11 +08:00
xiaojunnuo 4091abdfd7 chore: lint fix 2026-08-02 21:06:33 +08:00
xiaojunnuo 27008cb44a Merge remote-tracking branch 'origin/v2' into v2-dev 2026-08-02 19:51:18 +08:00
aheandGitHub 319f555569 Feature/偏好设置优化 (#775) @nicheng-he
* feat: 偏好设置支持从剪切板导入

在现有「复制偏好设置」基础上增加从剪切板导入,便于在浏览器间同步主题与布局配置。

* feat: 偏好设置支持保存到账号并在登录后自动同步

增加「保存到账号」能力,将本地偏好差异写入用户设置;登录成功后自动从账号拉取并应用到本地。
2026-08-02 04:27:07 +08:00
xiaojunnuo 83a6aed625 chore: 优化腾讯云删除证书提示 2026-07-30 19:08:57 +08:00
xiaojunnuo f1b67049d1 perf: 证书成功通知增加有效期显示 2026-07-28 19:22:12 +08:00
xiaojunnuo 8cbca5761e chore(pipeline): adjust npm install log truncation logic
change log output to take last 2000 chars instead of first 2000 for both stdout/stderr, and add consistent empty log placeholder text
2026-07-27 00:35:00 +08:00
xiaojunnuo 967846bef5 perf(tencent-clb): 为腾讯云CLB部署插件支持远程选择输入
- 重构插件输入配置,使用createRemoteSelectInputDefine统一封装远程选择组件
- 新增onGetCLBList、onGetListenerList、onGetDomainList方法实现数据拉取和格式转换
- 补全完整的单元测试用例,覆盖输入配置校验和数据映射逻辑
2026-07-26 19:01:18 +08:00
xiaojunnuo 18b2d3ac20 perf: 优化第三方依赖安装失败日志输出 2026-07-23 18:35:04 +08:00
xiaojunnuo 7d22fe3d7d perf: 优化SqliteError: disk I/O error报错修复提示 2026-07-23 10:49:00 +08:00
xiaojunnuo ee67b6c042 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-07-21 14:13:16 +08:00
xiaojunnuo 1cfa76683b perf: 优化宝塔报错提示 2026-07-21 14:13:07 +08:00
xiaojunnuo 4662e45e58 build: release 2026-07-19 01:28:05 +08:00
xiaojunnuo 1fefbdc9ab build: publish 2026-07-19 01:14:35 +08:00
xiaojunnuo 1cb2a57c55 build: trigger build image 2026-07-19 01:14:24 +08:00
91 changed files with 1242 additions and 497 deletions
+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.42.6](https://github.com/certd/certd/compare/v1.42.5...v1.42.6) (2026-07-18)
### Bug Fixes
* 修复正常批量删除流水线报权限不足的bug ([5b50083](https://github.com/certd/certd/commit/5b500830a122c6c42dab054e57fed509050f94da))
### Performance Improvements
* 优化动态加载依赖镜像地址,多次重试 ([5f53b81](https://github.com/certd/certd/commit/5f53b81c75dd242b4260ac08cae14c6d1a08a883))
## [1.42.5](https://github.com/certd/certd/compare/v1.42.4...v1.42.5) (2026-07-15)
### Bug Fixes
+2 -2
View File
@@ -50,7 +50,7 @@
"scripts": {
"before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
"build": "npm run before-build && tsc -p tsconfig.build.json --skipLibCheck",
"lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
"lint": "eslint --fix \"src/**/*.ts\" \"types/**/*.ts\"",
"lint-types": "tsd --files \"types/index.test-d.ts\"",
"prepublishOnly": "npm run build",
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
@@ -75,5 +75,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+2 -2
View File
@@ -17,7 +17,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"async-lock": "^1.4.1",
@@ -54,5 +54,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+5 -2
View File
@@ -11,9 +11,12 @@ export class LocalCache<V = any> {
cache: Map<string, { value: V; expiresAt: number }>;
constructor(opts: { clearInterval?: number } = {}) {
this.cache = new Map();
const intervalId = setInterval(() => {
const intervalId = setInterval(
() => {
this.clearExpires();
}, opts.clearInterval ?? 5 * 60 * 1000);
},
opts.clearInterval ?? 5 * 60 * 1000
);
intervalId.unref?.();
}
+2 -2
View File
@@ -1,4 +1,4 @@
export function isDev() {
const nodeEnv = process.env.NODE_ENV || 'dev';
return nodeEnv === 'development' || nodeEnv.includes('local') || nodeEnv.startsWith('dev');
const nodeEnv = process.env.NODE_ENV || "dev";
return nodeEnv === "development" || nodeEnv.includes("local") || nodeEnv.startsWith("dev");
}
+2 -2
View File
@@ -1,8 +1,8 @@
import fs from 'fs';
import fs from "fs";
function getFileRootDir(rootDir?: string) {
if (rootDir == null) {
const userHome = process.env.HOME || process.env.USERPROFILE;
rootDir = userHome + '/.certd/storage/';
rootDir = userHome + "/.certd/storage/";
}
if (!fs.existsSync(rootDir)) {
+1 -1
View File
@@ -1,2 +1,2 @@
import mitt from 'mitt';
import mitt from "mitt";
export const mitter = mitt();
+2 -2
View File
@@ -18,7 +18,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.42.6",
@@ -51,5 +51,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -568,11 +568,18 @@ export class RuntimeDepsService {
const result = await this.commandRunner.run(command, args, { cwd: rootDir, timeoutMs: this.installTimeoutMs, env: tryEnv });
if (result.code === 0) {
this.writeInstallState(statePath, { installedAt: new Date().toISOString(), registryUrl: tryUrl, dependenciesHash, nodeVersion: process.version, pnpmVersion, lockFileExists: fs.existsSync(lockPath) });
log.info(`${result.stdout?.slice(-2000) || "无npm安装日志输出"}`);
log.info("第三方依赖安装完成");
return { registryUrl: tryUrl, packageJsonPath };
}
lastError = result.stderr || result.stdout || "unknown error";
log.warn?.(`镜像 ${tryUrl || "默认"} 安装失败${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
const errOutput = (result.stderr || "").trim();
const outOutput = (result.stdout || "").trim();
lastError = errOutput || outOutput || "unknown error";
log.info(`镜像 ${tryUrl || "默认"} 安装失败,退出码: ${result.code}${urlsToTry.length > 1 ? ",尝试下一个镜像..." : ""}`);
log.info(` pnpm stderr: ${(errOutput || "无npm安装日志输出").slice(-2000)}`);
if (outOutput) {
log.info(` pnpm stdout: ${outOutput.slice(-2000)}`);
}
}
this.writeInstallState(statePath, {
...currentState,
+2 -2
View File
@@ -15,7 +15,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"axios": "^1.9.0",
@@ -31,5 +31,5 @@
"prettier": "3.3.3",
"tslib": "^2.8.1"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+2 -2
View File
@@ -18,7 +18,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"nanoid": "^5.0.7"
@@ -37,5 +37,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+1 -1
View File
@@ -1 +1 @@
export * from './lib/iframe.client.js';
export * from "./lib/iframe.client.js";
@@ -1,4 +1,4 @@
import { nanoid } from 'nanoid';
import { nanoid } from "nanoid";
export type IframeMessageData<T> = {
action: string;
@@ -32,7 +32,7 @@ export class IframeClient {
constructor(iframe?: HTMLIFrameElement, onError?: (e: any) => void) {
this.iframe = iframe;
this.onError = onError;
window.addEventListener('message', async (event: MessageEvent<IframeMessageData<any>>) => {
window.addEventListener("message", async (event: MessageEvent<IframeMessageData<any>>) => {
const data = event.data;
if (data.action) {
console.log(`收到消息[isSub:${this.isInFrame()}]`, data);
@@ -40,20 +40,20 @@ export class IframeClient {
const handler = this.handlers[data.action];
if (handler) {
const res = await handler(data);
if (data.id && data.action !== 'reply') {
await this.send('reply', res, data.id);
if (data.id && data.action !== "reply") {
await this.send("reply", res, data.id);
}
} else {
throw new Error(`action:${data.action} 未注册处理器,可能版本过低`);
}
} catch (e: any) {
console.error(e);
await this.send('reply', {}, data.id, 500, e.message);
await this.send("reply", {}, data.id, 500, e.message);
}
}
});
this.register('reply', async data => {
this.register("reply", async data => {
const req = this.requestQueue[data.replyId!];
if (req) {
req.onReply(data);
@@ -106,12 +106,12 @@ export class IframeClient {
console.log(`send message[isSub:${this.isInFrame()}]:`, reqMessageData);
if (!this.iframe) {
if (!window.parent) {
reject('当前页面不在 iframe 中');
reject("当前页面不在 iframe 中");
}
window.parent.postMessage(reqMessageData, '*');
window.parent.postMessage(reqMessageData, "*");
} else {
//子页面
this.iframe.contentWindow?.postMessage(reqMessageData, '*');
this.iframe.contentWindow?.postMessage(reqMessageData, "*");
}
} catch (e) {
console.error(e);
+2 -2
View File
@@ -13,7 +13,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"author": "",
"license": "Apache",
@@ -63,5 +63,5 @@
"fetch"
]
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+5 -5
View File
@@ -1,9 +1,9 @@
import jdCloud from "./lib/core.js";
import jdService from './lib/service.js'
import jdCloud from './lib/core.js';
import jdService from './lib/service.js';
import domainService from './repo/domainservice/v2/domainservice.js'
import cdnService from './repo/cdn/v1/cdn.js'
import sslService from './repo/ssl/v1/ssl.js'
import domainService from './repo/domainservice/v2/domainservice.js';
import cdnService from './repo/cdn/v1/cdn.js';
import sslService from './repo/ssl/v1/ssl.js';
export const JDCloud = jdCloud;
export const JDService = jdService;
export const JDDomainService = domainService;
+2 -2
View File
@@ -18,7 +18,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/basic": "^1.42.6",
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -10,6 +10,7 @@ export type K8sClientOpts = {
//暂时没用
lookup?: any;
skipTLSVerify?: boolean;
debug?: boolean;
};
export class K8sClient {
kubeconfig!: KubeConfig;
@@ -18,11 +19,13 @@ export class K8sClient {
client!: CoreV1Api;
logger: ILogger;
skipTLSVerify?: boolean;
debug?: boolean;
constructor(opts: K8sClientOpts) {
this.kubeConfigStr = opts.kubeConfigStr;
this.logger = opts.logger;
this.setLookup(opts.lookup);
this.skipTLSVerify = opts.skipTLSVerify;
this.debug = opts.debug;
this.init();
}
@@ -86,6 +89,9 @@ export class K8sClient {
yml.metadata = {};
}
yml.metadata.resourceVersion = existing.body.metadata.resourceVersion;
if (this.debug) {
this.logger.info("patch yaml body:", JSON.stringify(yml));
}
const res = await client.patch(yml);
return res?.body;
}
@@ -126,6 +132,9 @@ export class K8sClient {
async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || "default";
this.logger.info("create secret:", opts.body.metadata);
if (this.debug) {
this.logger.info("create secret body:", JSON.stringify(opts.body));
}
const created = await this.client.createNamespacedSecret(namespace, opts.body);
this.logger.info("new secrets:", opts.body.metadata);
return created.body;
@@ -162,6 +171,9 @@ export class K8sClient {
},
opts.body
);
if (this.debug) {
this.logger.info("create secret:", JSON.stringify(body));
}
const res = await this.createSecret({ namespace, body });
this.logger.info(`secret ${secretName} 已创建`);
return res;
@@ -173,6 +185,9 @@ export class K8sClient {
}
const newSecret = merge(oldSecret.body, opts.body);
if (this.debug) {
this.logger.info("patch secret:", JSON.stringify(newSecret));
}
const res = await this.client.replaceNamespacedSecret(secretName, namespace, newSecret);
this.logger.info(`secret ${secretName} 已更新`);
return res.body;
@@ -207,6 +222,9 @@ export class K8sClient {
const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
const oldIngress = await client.readNamespacedIngress(ingressName, namespace);
const newIngress = merge(oldIngress.body, opts.body);
if (this.debug) {
this.logger.info("patch ingress:", JSON.stringify(newIngress));
}
const res = await client.replaceNamespacedIngress(ingressName, namespace, newIngress);
this.logger.info("ingress patched", opts.body);
@@ -222,6 +240,7 @@ export class K8sClient {
},
};
for (const ingress of ingressNames) {
this.logger.info(`ingress 开始重启:${ingress}`);
await this.patchIngress({ namespace, ingressName: ingress, body });
this.logger.info(`ingress已重启:${ingress}`);
}
+2 -2
View File
@@ -19,7 +19,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"keywords": [],
"author": "greper",
@@ -69,5 +69,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+32 -32
View File
@@ -1,21 +1,21 @@
export const Constants = {
dataDir: './data',
dataDir: "./data",
role: {
defaultUser: 3,
},
per: {
//无需登录
guest: '_guest_',
guest: "_guest_",
//无需登录
anonymous: '_guest_',
anonymous: "_guest_",
//无需登录,有 token 时解析当前用户
guestOptionalAuth: '_guestOptionalAuth_',
guestOptionalAuth: "_guestOptionalAuth_",
//仅需要登录
authOnly: '_authOnly_',
authOnly: "_authOnly_",
//仅需要登录
loginOnly: '_authOnly_',
loginOnly: "_authOnly_",
open: '_open_',
open: "_open_",
},
res: {
serverError(message: string) {
@@ -26,102 +26,102 @@ export const Constants = {
},
error: {
code: 1,
message: 'Internal server error',
message: "Internal server error",
},
success: {
code: 0,
message: 'success',
message: "success",
},
validation: {
code: 10,
message: '参数错误',
message: "参数错误",
},
needvip: {
code: 88,
message: '需要VIP',
message: "需要VIP",
},
needsuite: {
code: 89,
message: '需要购买或升级套餐',
message: "需要购买或升级套餐",
},
loginError: {
code: 2,
message: '登录失败',
message: "登录失败",
},
codeError: {
code: 3,
message: '验证码错误',
message: "验证码错误",
},
auth: {
code: 401,
message: '您还未登录或token已过期',
message: "您还未登录或token已过期",
},
permission: {
code: 402,
message: '您没有权限',
message: "您没有权限",
},
param: {
code: 400,
message: '参数错误',
message: "参数错误",
},
notFound: {
code: 404,
message: '页面/文件/资源不存在',
message: "页面/文件/资源不存在",
},
preview: {
code: 10001,
message: '对不起,预览环境不允许修改此数据',
message: "对不起,预览环境不允许修改此数据",
},
siteOff: {
code: 10010,
message: '站点已关闭',
message: "站点已关闭",
},
need2fa: {
code: 10020,
message: '需要2FA认证',
message: "需要2FA认证",
},
openKeyError: {
code: 20000,
message: 'ApiToken错误',
message: "ApiToken错误",
},
openKeySignError: {
code: 20001,
message: 'ApiToken签名错误',
message: "ApiToken签名错误",
},
openKeyExpiresError: {
code: 20002,
message: 'ApiToken时间戳错误',
message: "ApiToken时间戳错误",
},
openKeySignTypeError: {
code: 20003,
message: 'ApiToken签名类型不支持',
message: "ApiToken签名类型不支持",
},
openParamError: {
code: 20010,
message: '请求参数错误',
message: "请求参数错误",
},
openCertNotFound: {
code: 20011,
message: '证书不存在',
message: "证书不存在",
},
openCertNotReady: {
code: 20012,
message: '证书还未生成',
message: "证书还未生成",
},
openCertApplying: {
code: 20013,
message: '证书正在申请中,请稍后重新获取',
message: "证书正在申请中,请稍后重新获取",
},
openDomainNoVerifier: {
code: 20014,
message: '域名校验方式未配置',
message: "域名校验方式未配置",
},
openEmailNotFound: {
code: 20021,
message: '用户邮箱还未配置',
message: "用户邮箱还未配置",
},
},
systemUserId: 0, // 系统级别userid固定为0
enterpriseUserId: -1 // 企业模式用户id固定为-1
enterpriseUserId: -1, // 企业模式用户id固定为-1
};
@@ -1,11 +1,11 @@
import { ALL, Body, Post, Query } from '@midwayjs/core';
import { BaseController } from './base-controller.js';
import { ALL, Body, Post, Query } from "@midwayjs/core";
import { BaseController } from "./base-controller.js";
export abstract class CrudController<T> extends BaseController {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
abstract getService<T>();
@Post('/page')
@Post("/page")
async page(@Body(ALL) body: any) {
const pageRet = await this.getService().page({
query: body.query ?? {},
@@ -16,7 +16,7 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(pageRet);
}
@Post('/list')
@Post("/list")
async list(@Body(ALL) body: any) {
const listRet = await this.getService().list({
query: body.query ?? {},
@@ -25,33 +25,33 @@ export abstract class CrudController<T> extends BaseController {
return this.ok(listRet);
}
@Post('/add')
@Post("/add")
async add(@Body(ALL) bean: any) {
delete bean.id;
const id = await this.getService().add(bean);
return this.ok(id);
}
@Post('/info')
async info(@Query('id') id: number) {
@Post("/info")
async info(@Query("id") id: number) {
const bean = await this.getService().info(id);
return this.ok(bean);
}
@Post('/update')
@Post("/update")
async update(@Body(ALL) bean: any) {
await this.getService().update(bean);
return this.ok(null);
}
@Post('/delete')
async delete(@Query('id') id: number) {
@Post("/delete")
async delete(@Query("id") id: number) {
await this.getService().delete([id]);
return this.ok(null);
}
@Post('/deleteByIds')
async deleteByIds(@Body('ids') ids: number[]) {
@Post("/deleteByIds")
async deleteByIds(@Body("ids") ids: number[]) {
await this.getService().delete(ids);
return this.ok(null);
}
@@ -1,19 +1,17 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
import { TextException } from "./common-exception.js";
/**
* 授权异常
*/
export class AuthException extends BaseException {
constructor(message?: string) {
super('AuthException', Constants.res.auth.code, message ? message : Constants.res.auth.message);
super("AuthException", Constants.res.auth.code, message ? message : Constants.res.auth.message);
}
}
export class Need2FAException extends TextException {
constructor(message: string, data: any) {
super('Need2FAException', Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message,data);
super("Need2FAException", Constants.res.need2fa.code, message ? message : Constants.res.need2fa.message, data);
}
}
@@ -3,7 +3,7 @@
*/
export class BaseException extends Error {
code: number;
data?:any
data?: any;
constructor(name: string, code: number, message: string, data?: any) {
super(message);
this.name = name;
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 验证码异常
*/
export class CodeErrorException extends BaseException {
constructor(message) {
super('CodeErrorException', Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
super("CodeErrorException", Constants.res.codeError.code, message ? message : Constants.res.codeError.message);
}
}
@@ -1,12 +1,13 @@
export * from './auth-exception.js';
export * from './base-exception.js';
export * from './permission-exception.js';
export * from './preview-exception.js';
export * from './validation-exception.js';
export * from './vip-exception.js';
export * from './common-exception.js';
export * from './not-found-exception.js';
export * from './param-exception.js';
export * from './site-off-exception.js';
export * from './login-error-exception.js'
export * from './code-error-exception.js'
export * from "./auth-exception.js";
export * from "./base-exception.js";
export * from "./permission-exception.js";
export * from "./preview-exception.js";
export * from "./validation-exception.js";
export * from "./vip-exception.js";
export * from "./common-exception.js";
export * from "./not-found-exception.js";
export * from "./param-exception.js";
export * from "./site-off-exception.js";
export * from "./login-error-exception.js";
export * from "./code-error-exception.js";
export * from "./non-retryable-exception.js";
@@ -1,12 +1,12 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 通用异常
*/
export class LoginErrorException extends BaseException {
leftCount: number;
constructor(message, leftCount: number) {
super('LoginErrorException', Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
super("LoginErrorException", Constants.res.loginError.code, message ? message : Constants.res.loginError.message);
this.leftCount = leftCount;
}
}
@@ -0,0 +1,12 @@
import assert from "assert";
import { NonRetryableException } from "./non-retryable-exception.js";
describe("NonRetryableException", () => {
it("sets the standard error name and message", () => {
const error = new NonRetryableException("cannot retry");
assert.equal(error.name, "NonRetryableException");
assert.equal(error.message, "cannot retry");
assert.equal(error instanceof Error, true);
});
});
@@ -0,0 +1,6 @@
export class NonRetryableException extends Error {
constructor(message: string) {
super(message);
this.name = "NonRetryableException";
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 资源不存在
*/
export class NotFoundException extends BaseException {
constructor(message) {
super('NotFoundException', Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
super("NotFoundException", Constants.res.notFound.code, message ? message : Constants.res.notFound.message);
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 参数异常
*/
export class ParamException extends BaseException {
constructor(message) {
super('ParamException', Constants.res.param.code, message ? message : Constants.res.param.message);
super("ParamException", Constants.res.param.code, message ? message : Constants.res.param.message);
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 授权异常
*/
export class PermissionException extends BaseException {
constructor(message?: string) {
super('PermissionException', Constants.res.permission.code, message ? message : Constants.res.permission.message);
super("PermissionException", Constants.res.permission.code, message ? message : Constants.res.permission.message);
}
}
@@ -1,14 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 预览模式
*/
export class PreviewException extends BaseException {
constructor(message) {
super(
'PreviewException',
Constants.res.preview.code,
message ? message : Constants.res.preview.message
);
super("PreviewException", Constants.res.preview.code, message ? message : Constants.res.preview.message);
}
}
@@ -1,9 +1,9 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
*/
export class SiteOffException extends BaseException {
constructor(message) {
super('SiteOffException', Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
super("SiteOffException", Constants.res.siteOff.code, message ? message : Constants.res.siteOff.message);
}
}
@@ -1,10 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 校验异常
*/
export class ValidateException extends BaseException {
constructor(message) {
super('ValidateException', Constants.res.validation.code, message ? message : Constants.res.validation.message);
super("ValidateException", Constants.res.validation.code, message ? message : Constants.res.validation.message);
}
}
@@ -1,16 +1,16 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
import { Constants } from "../constants.js";
import { BaseException } from "./base-exception.js";
/**
* 需要vip异常
*/
export class NeedVIPException extends BaseException {
constructor(message) {
super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
super("NeedVIPException", Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
}
}
export class NeedSuiteException extends BaseException {
constructor(message) {
super('NeedSuiteException', Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
super("NeedSuiteException", Constants.res.needsuite.code, message ? message : Constants.res.needsuite.message);
}
}
+8 -8
View File
@@ -1,8 +1,8 @@
export * from './base-controller.js';
export * from './constants.js';
export * from './crud-controller.js';
export * from './enum-item.js';
export * from './exception/index.js';
export * from './result.js';
export * from './base-service.js';
export * from "./mode.js"
export * from "./base-controller.js";
export * from "./constants.js";
export * from "./crud-controller.js";
export * from "./enum-item.js";
export * from "./exception/index.js";
export * from "./result.js";
export * from "./base-service.js";
export * from "./mode.js";
+4 -4
View File
@@ -1,12 +1,12 @@
let adminMode = "saas"
let adminMode = "saas";
export function setAdminMode(mode: string = "saas") {
adminMode = mode
adminMode = mode;
}
export function getAdminMode() {
return adminMode
return adminMode;
}
export function isEnterprise() {
return adminMode === "enterprise"
return adminMode === "enterprise";
}
@@ -1,11 +1,11 @@
import type { IMidwayContainer } from '@midwayjs/core';
import { Configuration } from '@midwayjs/core';
import { logger } from '@certd/basic';
import type { IMidwayContainer } from "@midwayjs/core";
import { Configuration } from "@midwayjs/core";
import { logger } from "@certd/basic";
@Configuration({
namespace: 'lib-server',
namespace: "lib-server",
})
export class LibServerConfiguration {
async onReady(container: IMidwayContainer) {
logger.info('lib start...');
logger.info("lib start...");
}
}
+6 -6
View File
@@ -1,9 +1,9 @@
import { SysSettingsEntity } from './system/index.js';
import { AccessEntity } from './user/access/entity/access.js';
import { SysSettingsEntity } from "./system/index.js";
import { AccessEntity } from "./user/access/entity/access.js";
import { AddonEntity } from "./user/index.js";
export * from './basic/index.js';
export * from './system/index.js';
export * from './user/index.js';
export { LibServerConfiguration as Configuration } from './configuration.js';
export * from "./basic/index.js";
export * from "./system/index.js";
export * from "./user/index.js";
export { LibServerConfiguration as Configuration } from "./configuration.js";
export const libServerEntities = [SysSettingsEntity, AccessEntity, AddonEntity];
@@ -1,5 +1,5 @@
export * from './service/plus-service.js';
export * from './service/file-service.js';
export * from './service/encryptor.js';
export * from './service/ocr-service.js';
export * from './service/executor-queue.js';
export * from "./service/plus-service.js";
export * from "./service/file-service.js";
export * from "./service/encryptor.js";
export * from "./service/ocr-service.js";
export * from "./service/executor-queue.js";
@@ -1,8 +1,8 @@
import crypto from 'crypto';
import crypto from "crypto";
export class Encryptor {
secretKey: Buffer;
constructor(encryptSecret: string, encoding: BufferEncoding = 'base64') {
constructor(encryptSecret: string, encoding: BufferEncoding = "base64") {
this.secretKey = Buffer.from(encryptSecret, encoding);
}
// 加密函数
@@ -10,18 +10,18 @@ export class Encryptor {
const iv = crypto.randomBytes(16); // 初始化向量
// const secretKey = crypto.randomBytes(32);
// const key = Buffer.from(secretKey);
const cipher = crypto.createCipheriv('aes-256-cbc', this.secretKey, iv);
const cipher = crypto.createCipheriv("aes-256-cbc", this.secretKey, iv);
let encrypted = cipher.update(text);
encrypted = Buffer.concat([encrypted, cipher.final()]);
return iv.toString('hex') + ':' + encrypted.toString('hex');
return iv.toString("hex") + ":" + encrypted.toString("hex");
}
// 解密函数
decrypt(encryptedText: string) {
const textParts = encryptedText.split(':');
const iv = Buffer.from(textParts.shift(), 'hex');
const encrypted = Buffer.from(textParts.join(':'), 'hex');
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(this.secretKey), iv);
const textParts = encryptedText.split(":");
const iv = Buffer.from(textParts.shift(), "hex");
const encrypted = Buffer.from(textParts.join(":"), "hex");
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.from(this.secretKey), iv);
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted.toString();
@@ -2,7 +2,7 @@ import { logger } from "@certd/basic";
export type TaskItem = {
task: () => Promise<void>;
}
};
export class UserTaskQueue {
userId: number;
@@ -10,7 +10,7 @@ export class UserTaskQueue{
runningQueue: TaskItem[] = [];
getMaxRunningCount: () => number;
constructor(req: { userId: number ,getMaxRunningCount: ()=>number }) {
constructor(req: { userId: number; getMaxRunningCount: () => number }) {
this.userId = req.userId;
this.getMaxRunningCount = req.getMaxRunningCount;
}
@@ -46,9 +46,9 @@ export class UserTaskQueue{
// 继续执行下一个任务
this.runTask();
}
}
};
logger.info(`[user_${this.userId}]执行任务,当前运行队列:${this.runningQueue.length}, 等待队列:${this.pendingQueue.length}`);
call()
call();
}
}
@@ -56,7 +56,6 @@ export class ExecutorQueue{
queues: Record<number, UserTaskQueue> = {};
maxRunningCount: number = 10;
setMaxRunningCount(count: number) {
this.maxRunningCount = count;
}
@@ -73,7 +72,6 @@ export class ExecutorQueue{
const userQueue = this.getUserQueue(userId);
userQueue.addTask(task);
}
}
export const executorQueue = new ExecutorQueue();
@@ -1,42 +1,42 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import dayjs from 'dayjs';
import path from 'path';
import fs from 'fs';
import { cache, logger, utils } from '@certd/basic';
import { NotFoundException, ParamException, PermissionException } from '../../../basic/index.js';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import dayjs from "dayjs";
import path from "path";
import fs from "fs";
import { cache, logger, utils } from "@certd/basic";
import { NotFoundException, ParamException, PermissionException } from "../../../basic/index.js";
export type UploadFileItem = {
filename: string;
tmpFilePath: string;
};
const uploadRootDir = './data/upload';
export const uploadTmpFileCacheKey = 'tmpfile_key_';
const uploadRootDir = "./data/upload";
export const uploadTmpFileCacheKey = "tmpfile_key_";
/**
*/
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class FileService {
async saveFile(userId: number, tmpCacheKey: any, permission: 'public' | 'private') {
async saveFile(userId: number, tmpCacheKey: any, permission: "public" | "private") {
if (tmpCacheKey.startsWith(`/${permission}`)) {
//已经保存过,不需要再次保存
return tmpCacheKey;
}
let fileName = '';
let fileName = "";
let tmpFilePath = tmpCacheKey;
if (uploadTmpFileCacheKey && tmpCacheKey.startsWith(uploadTmpFileCacheKey)) {
const tmpFile: UploadFileItem = cache.get(tmpCacheKey);
if (!tmpFile) {
throw new ParamException('文件已过期,请重新上传');
throw new ParamException("文件已过期,请重新上传");
}
tmpFilePath = tmpFile.tmpFilePath;
fileName = tmpFile.filename || path.basename(tmpFilePath);
}
if (!tmpFilePath || !fs.existsSync(tmpFilePath)) {
throw new Error('文件不存在,请重新上传');
throw new Error("文件不存在,请重新上传");
}
const date = dayjs().format('YYYY_MM_DD');
const date = dayjs().format("YYYY_MM_DD");
const random = Math.random().toString(36).substring(7);
const userIdMd5 = Buffer.from(Buffer.from(userId + '').toString('base64')).toString('hex');
const userIdMd5 = Buffer.from(Buffer.from(userId + "").toString("base64")).toString("hex");
const key = `/${permission}/${userIdMd5}/${date}/${random}_${fileName}`;
let savePath = path.join(uploadRootDir, key);
savePath = path.resolve(savePath);
@@ -44,7 +44,6 @@ export class FileService {
if (!fs.existsSync(parentDir)) {
fs.mkdirSync(parentDir, { recursive: true });
}
// eslint-disable-next-line node/no-unsupported-features/node-builtins
const copyFile = utils.promises.promisify(fs.copyFile);
await copyFile(tmpFilePath, savePath);
try {
@@ -58,29 +57,29 @@ export class FileService {
getFile(key: string, userId?: number, allowAnyPrivateUser = false) {
if (!key) {
throw new ParamException('参数错误');
throw new ParamException("参数错误");
}
if (key.indexOf('..') >= 0) {
if (key.indexOf("..") >= 0) {
//安全性判断
throw new ParamException('参数错误');
throw new ParamException("参数错误");
}
if (!key.startsWith('/')) {
throw new ParamException('参数错误');
if (!key.startsWith("/")) {
throw new ParamException("参数错误");
}
const keyArr = key.split('/');
const keyArr = key.split("/");
const permission = keyArr[1];
const userIdMd5 = keyArr[2];
if (permission !== 'public' && !allowAnyPrivateUser) {
if (permission !== "public" && !allowAnyPrivateUser) {
//非公开文件需要验证用户
const userIdStr = Buffer.from(Buffer.from(userIdMd5, 'hex').toString('base64')).toString();
const userIdStr = Buffer.from(Buffer.from(userIdMd5, "hex").toString("base64")).toString();
const userIdInt: number = parseInt(userIdStr, 10);
if (userId == null || userIdInt !== userId) {
throw new PermissionException('无访问权限');
throw new PermissionException("无访问权限");
}
}
const filePath = path.join(uploadRootDir, key);
if (!fs.existsSync(filePath)) {
throw new NotFoundException('文件不存在');
throw new NotFoundException("文件不存在");
}
return filePath;
}
@@ -15,10 +15,9 @@ export class OcrService implements IOcrService {
url: "/activation/certd/ocr",
method: "post",
data: {
image: opts.image
}
image: opts.image,
},
});
return res;
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './settings/index.js';
export * from './basic/index.js';
export * from "./settings/index.js";
export * from "./basic/index.js";
@@ -1,3 +1,3 @@
export * from './service/sys-settings-service.js';
export * from './service/models.js';
export * from './entity/sys-settings.js';
export * from "./service/sys-settings-service.js";
export * from "./service/models.js";
export * from "./entity/sys-settings.js";
@@ -1,15 +1,15 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import { SysSettingsEntity } from '../entity/sys-settings.js';
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from './models.js';
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { SysSettingsEntity } from "../entity/sys-settings.js";
import { BaseSettings, SysInstallInfo, SysPrivateSettings, SysPublicSettings, SysSecret, SysSecretBackup } from "./models.js";
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from '@certd/acme-client';
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from '@certd/basic';
import { isPlus } from '@certd/plus-core';
import * as dns from 'node:dns';
import { BaseService, setAdminMode } from '../../../basic/index.js';
import { executorQueue } from '../../basic/service/executor-queue.js';
import { getAllSslProviderDomains, setSslProviderReverseProxies, setWalkFromAuthoritative } from "@certd/acme-client";
import { cache, logger, mergeUtils, setGlobalHeaders, setGlobalProxy } from "@certd/basic";
import { isPlus } from "@certd/plus-core";
import * as dns from "node:dns";
import { BaseService, setAdminMode } from "../../../basic/index.js";
import { executorQueue } from "../../basic/service/executor-queue.js";
const { merge } = mergeUtils;
let lastSaveEnvVars = {};
@@ -138,7 +138,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
res.reverseProxies[domain] = "";
}
}
return res
return res;
}
async savePrivateSettings(bean: SysPrivateSettings) {
@@ -149,14 +149,14 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
}
async reloadSettings() {
await this.reloadPrivateSettings()
await this.reloadPublicSettings()
await this.reloadPrivateSettings();
await this.reloadPublicSettings();
}
async reloadPublicSettings() {
const publicSetting = await this.getPublicSettings()
const publicSetting = await this.getPublicSettings();
if (isPlus()) {
setAdminMode(publicSetting.adminMode )
setAdminMode(publicSetting.adminMode);
}
}
@@ -183,29 +183,28 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
this.setEnvironmentVars(privateSetting.environmentVars);
setWalkFromAuthoritative(privateSetting.acmeWalkFromAuthoritative);
}
parseKeyValueText(text: string) {
const values = {};
if (typeof text !== 'string') {
if (typeof text !== "string") {
text = "";
}
text.split('\n').forEach(line => {
text.split("\n").forEach(line => {
line = line.trim();
if (!line || line.startsWith('#')) {
return
if (!line || line.startsWith("#")) {
return;
}
const arr = line.split("#")
const arr = line.split("#");
if (arr.length > 0) {
line = arr[0].trim();
}
if (!line.includes("=")) {
return
return;
}
const eqIndex = line.indexOf('=');
const eqIndex = line.indexOf("=");
const key = line.substring(0, eqIndex).trim();
const value = line.substring(eqIndex + 1).trim();
if (key && value) {
@@ -234,7 +233,7 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
entity.setting = JSON.stringify(setting);
await this.repository.save(entity);
} else {
throw new Error('该设置不存在');
throw new Error("该设置不存在");
}
cache.delete(`settings.${key}`);
}
@@ -246,20 +245,20 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
if (settings == null) {
const backup = new SysSecretBackup();
if (installInfo.siteId == null || privateSettings.encryptSecret == null) {
logger.error('备份密钥失败,siteId或encryptSecret为空');
logger.error("备份密钥失败,siteId或encryptSecret为空");
return;
}
backup.siteId = installInfo.siteId;
backup.encryptSecret = privateSettings.encryptSecret;
await this.saveSetting(backup);
logger.info('备份密钥成功');
logger.info("备份密钥成功");
} else {
//校验是否有变化
if (settings.siteId !== installInfo.siteId) {
throw new Error(`siteId与备份不一致,可能是数据异常,请检查:backup=${settings.siteId}, current=${installInfo.siteId}`);
}
if (settings.encryptSecret !== privateSettings.encryptSecret) {
throw new Error('encryptSecret与备份不一致,可能是数据异常,请检查');
throw new Error("encryptSecret与备份不一致,可能是数据异常,请检查");
}
}
}
@@ -271,12 +270,12 @@ export class SysSettingsService extends BaseService<SysSettingsEntity> {
//从备份中读取
const settings = await this.getSettingByKey(SysSecretBackup.__key__);
if (settings == null || !settings.encryptSecret) {
throw new Error('密钥备份不存在');
throw new Error("密钥备份不存在");
}
sysSecret.siteId = settings.siteId;
sysSecret.encryptSecret = settings.encryptSecret;
await this.saveSetting(sysSecret);
logger.info('密钥恢复成功');
logger.info("密钥恢复成功");
return sysSecret;
}
}
@@ -1,46 +1,46 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
* 授权配置
*/
@Entity('cd_access')
@Entity("cd_access")
export class AccessEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
@Column({ name: "key_id", comment: "key_id", length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number; // 0为系统级别, -1为企业,大于1为用户
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
name: string;
@Column({ comment: '类型', length: 100 })
@Column({ comment: "类型", length: 100 })
type: string;
@Column({ name: 'subtype', comment: '子类型', length: 100, nullable: true })
@Column({ name: "subtype", comment: "子类型", length: 100, nullable: true })
subtype: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true })
setting: string;
@Column({ name: 'encrypt_setting', comment: '已加密设置', length: 10240, nullable: true })
@Column({ name: "encrypt_setting", comment: "已加密设置", length: 10240, nullable: true })
encryptSetting: string;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,5 +1,5 @@
export * from './entity/access.js';
export * from './service/access-service.js';
export * from './service/access-sys-getter.js';
export * from './service/access-getter.js';
export * from './service/encrypt-service.js';
export * from "./entity/access.js";
export * from "./service/access-service.js";
export * from "./service/access-sys-getter.js";
export * from "./service/access-getter.js";
export * from "./service/encrypt-service.js";
@@ -1,5 +1,5 @@
import { IAccessService } from '@certd/pipeline';
import { AccessService } from './access-service.js';
import { IAccessService } from "@certd/pipeline";
import { AccessService } from "./access-service.js";
export class AccessSysGetter implements IAccessService {
accessService: AccessService;
@@ -1,5 +1,5 @@
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { Encryptor, SysSecret, SysSettingsService } from '../../../system/index.js';
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { Encryptor, SysSecret, SysSettingsService } from "../../../system/index.js";
/**
* 授权
@@ -49,7 +49,7 @@ export function AddonInput(input?: AddonInputDefine): PropertyDecorator {
}
export async function newAddon(addonType: string, type: string, input: any, ctx: AddonContext) {
const key = `${addonType}:${type}`
const key = `${addonType}:${type}`;
const register = addonRegistry.get(key);
if (register == null) {
throw new Error(`${addonType} ${type} not found`);
@@ -1,49 +1,46 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
/**
*/
@Entity('cd_addon')
@Entity("cd_addon")
export class AddonEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ name: 'key_id', comment: 'key_id', length: 100 })
@Column({ name: "key_id", comment: "key_id", length: 100 })
keyId: string;
@Column({ name: 'user_id', comment: '用户id' })
@Column({ name: "user_id", comment: "用户id" })
userId: number;
@Column({ comment: '名称', length: 100 })
@Column({ comment: "名称", length: 100 })
name: string;
@Column({ name: 'addon_type', comment: 'addon类型', length: 100 })
@Column({ name: "addon_type", comment: "addon类型", length: 100 })
addonType: string;
@Column({ comment: '类型', length: 100 })
@Column({ comment: "类型", length: 100 })
type: string;
@Column({ name: 'setting', comment: '设置', length: 10240, nullable: true })
@Column({ name: "setting", comment: "设置", length: 10240, nullable: true })
setting: string;
@Column({ name: 'is_system', comment: '是否系统级别', nullable: false, default: false })
@Column({ name: "is_system", comment: "是否系统级别", nullable: false, default: false })
isSystem: boolean;
@Column({ name: 'is_default', comment: '是否默认', nullable: false, default: false })
@Column({ name: "is_default", comment: "是否默认", nullable: false, default: false })
isDefault: boolean;
@Column({ name: 'project_id', comment: '项目id' })
@Column({ name: "project_id", comment: "项目id" })
projectId: number;
@Column({
name: 'create_time',
comment: '创建时间',
default: () => 'CURRENT_TIMESTAMP',
name: "create_time",
comment: "创建时间",
default: () => "CURRENT_TIMESTAMP",
})
createTime: Date;
@Column({
name: 'update_time',
comment: '修改时间',
default: () => 'CURRENT_TIMESTAMP',
name: "update_time",
comment: "修改时间",
default: () => "CURRENT_TIMESTAMP",
})
updateTime: Date;
}
@@ -1,3 +1,3 @@
export * from './api/index.js'
export * from './entity/addon.js'
export * from './service/addon-service.js'
export * from "./api/index.js";
export * from "./entity/addon.js";
export * from "./service/addon-service.js";
@@ -49,7 +49,6 @@ export class AddonService extends BaseService<AddonEntity> {
return await super.add(param);
}
/**
*
* @param param
@@ -59,7 +58,7 @@ export class AddonService extends BaseService<AddonEntity> {
if (oldEntity == null) {
throw new ValidateException("该Addon配置不存在,请确认是否已被删除");
}
delete param.keyId
delete param.keyId;
return await super.update(param);
}
@@ -75,11 +74,10 @@ export class AddonService extends BaseService<AddonEntity> {
userId: entity.userId,
addonType: entity.addonType,
type: entity.type,
projectId: entity.projectId
projectId: entity.projectId,
};
}
getDefineList(addonType: string) {
return addonRegistry.getDefineList(addonType);
}
@@ -88,7 +86,6 @@ export class AddonService extends BaseService<AddonEntity> {
return addonRegistry.getDefine(type, prefix) as AddonDefine;
}
async getSimpleByIds(ids: number[], userId: any, projectId?: number) {
if (ids.length === 0) {
return [];
@@ -109,13 +106,11 @@ export class AddonService extends BaseService<AddonEntity> {
addonType: true,
type: true,
userId: true,
isSystem: true
}
isSystem: true,
},
});
}
async getDefault(userId: number, addonType: string, projectId?: number): Promise<any> {
const userProjectQuery = this.buildUserProjectQuery(userId, projectId);
const res = await this.repository.findOne({
@@ -124,8 +119,8 @@ export class AddonService extends BaseService<AddonEntity> {
...userProjectQuery,
},
order: {
isDefault: "DESC"
}
isDefault: "DESC",
},
});
if (!res) {
return null;
@@ -143,7 +138,7 @@ export class AddonService extends BaseService<AddonEntity> {
name: res.name,
userId: res.userId,
setting,
projectId: res.projectId
projectId: res.projectId,
};
}
@@ -160,14 +155,17 @@ export class AddonService extends BaseService<AddonEntity> {
...userProjectQuery,
};
await this.repository.update(query, {
isDefault: false
});
await this.repository.update({ ...query, id }, {
isDefault: true
isDefault: false,
});
await this.repository.update(
{ ...query, id },
{
isDefault: true,
}
);
}
async getOrCreateDefault(opts: { addonType: string, type: string, inputs: any, userId: any,projectId?:number }) {
async getOrCreateDefault(opts: { addonType: string; type: string; inputs: any; userId: any; projectId?: number }) {
const { addonType, type, inputs, userId, projectId } = opts;
const addonDefine = this.getDefineByType(type, addonType);
@@ -177,7 +175,7 @@ export class AddonService extends BaseService<AddonEntity> {
return defaultConfig;
}
const setting = {
...inputs
...inputs,
};
const res = await this.repository.save({
userId,
@@ -186,19 +184,19 @@ export class AddonService extends BaseService<AddonEntity> {
name: addonDefine.title,
setting: JSON.stringify(setting),
isDefault: true,
projectId
projectId,
});
return this.buildAddonInstanceConfig(res);
}
async getOneByType(req:{addonType:string,type:string,userId:number,projectId?:number}) {
async getOneByType(req: { addonType: string; type: string; userId: number; projectId?: number }) {
const userProjectQuery = this.buildUserProjectQuery(req.userId, req.projectId);
return await this.repository.findOne({
where: {
addonType: req.addonType,
type: req.type,
...userProjectQuery,
}
},
});
}
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './access/index.js';
export * from './addon/index.js';
export * from "./access/index.js";
export * from "./addon/index.js";
+2 -2
View File
@@ -19,7 +19,7 @@
"pub": "npm publish",
"compile": "npm run build",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"keywords": [],
"author": "greper",
@@ -52,5 +52,5 @@
"typeorm": "^0.3.20",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -1,11 +1,11 @@
import { Config, Configuration, Logger } from '@midwayjs/core';
import { Flyway } from './flyway.js';
import type { ILogger } from '@midwayjs/logger';
import { TypeORMDataSourceManager } from '@midwayjs/typeorm';
import type { IMidwayContainer } from '@midwayjs/core';
import { Config, Configuration, Logger } from "@midwayjs/core";
import { Flyway } from "./flyway.js";
import type { ILogger } from "@midwayjs/logger";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
import type { IMidwayContainer } from "@midwayjs/core";
@Configuration({
namespace: 'flyway',
namespace: "flyway",
//importConfigs: [join(__dirname, './config')],
})
export class FlywayConfiguration {
@@ -14,9 +14,9 @@ export class FlywayConfiguration {
@Logger()
logger!: ILogger;
async onReady(container: IMidwayContainer) {
this.logger.info('flyway start:' + JSON.stringify(this.flyway));
this.logger.info("flyway start:" + JSON.stringify(this.flyway));
const dataSourceManager = await container.getAsync(TypeORMDataSourceManager);
const dataSourceName = this.flyway.dataSourceName || 'default';
const dataSourceName = this.flyway.dataSourceName || "default";
const connection = dataSourceManager.getDataSource(dataSourceName);
await new Flyway({ ...this.flyway, logger: this.logger, connection }).run();
}
+6 -6
View File
@@ -1,23 +1,23 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity('flyway_history')
@Entity("flyway_history")
export class FlywayHistory {
@PrimaryGeneratedColumn()
id?: number;
@Column({ comment: '文件名', length: 100 })
@Column({ comment: "文件名", length: 100 })
name?: string;
@Column({ comment: 'hash', length: 32 })
@Column({ comment: "hash", length: 32 })
hash?: string;
@Column({
comment: '执行时间',
comment: "执行时间",
})
timestamp?: Date;
@Column({
comment: '执行成功',
comment: "执行成功",
default: true,
})
success?: boolean;
+8 -5
View File
@@ -93,19 +93,21 @@ export class Flyway {
await queryRunner.commitTransaction();
} catch (err) {
this.logger.error(err);
this.errorTip(err);
await this.storeSqlExecLog(file.script, filepath, false, queryRunner);
await queryRunner.rollbackTransaction();
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)", file.script);
}
throw err;
}
}
this.logger.info("[ midfly ] end-------------");
}
private errorTip(err: any) {
if (err.code === "SQLITE_IOERR_WRITE") {
this.logger.warn("SQLite数据库写入失败,可能您的操作系统版本太低,请将「certd:latest」镜像改为「certd:slim」即可。(如需指定版本可以修改成「certd:[version]-slim」)");
}
}
private async storeSqlExecLog(filename: string, filepath: string, success: boolean, queryRunner: QueryRunner) {
const hash = await this.getFileHash(filepath);
//先删除
@@ -265,6 +267,7 @@ export class Flyway {
await queryRunner.query(sql);
} catch (err: any) {
this.logger.error("exec sql error ", err.message, err);
this.errorTip(err);
throw err;
}
}
+3 -3
View File
@@ -1,3 +1,3 @@
export { FlywayConfiguration as Configuration } from './configuration.js';
export { Flyway, setFlywayLogger } from './flyway.js';
export { FlywayHistory } from './entity.js';
export { FlywayConfiguration as Configuration } from "./configuration.js";
export { Flyway, setFlywayLogger } from "./flyway.js";
export { FlywayHistory } from "./entity.js";
+2 -2
View File
@@ -17,7 +17,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/plugin-lib": "^1.42.6"
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
+2 -2
View File
@@ -14,7 +14,7 @@
"pub": "npm publish",
"compile": "tsc --skipLibCheck --watch",
"format": "prettier --write src",
"lint": "eslint --fix"
"lint": "eslint --fix --ext .ts src"
},
"dependencies": {
"@certd/acme-client": "^1.42.6",
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "83495b32138cf831b4ea02054d12981d9787ebf0"
"gitHead": "246ee83015bf5589adc2a5fa3d1388c8d9a2a252"
}
@@ -64,6 +64,7 @@ export default {
projectUserManager: "Project User Management",
myProjectManager: "My Projects",
myProjectDetail: "Project Detail",
projectDetail: "Project Detail",
projectJoin: "Join Project",
currentProject: "Current Project",
projectMemberManager: "Project Member",
@@ -31,6 +31,15 @@ export default {
copyPreferences: "Copy Preferences",
copyPreferencesSuccessTitle: "Copy successful",
copyPreferencesSuccess: "Copy successful, please override in `src/preferences.ts` under app",
importPreferences: "Import from Clipboard",
importPreferencesSuccessTitle: "Import successful",
importPreferencesSuccess: "Preferences imported from clipboard",
importPreferencesErrorTitle: "Import failed",
importPreferencesError: "Clipboard content is invalid. Please copy preferences JSON first",
saveToAccount: "Save to Account",
saveToAccountSuccess: "Preferences saved to account",
saveToAccountError: "Failed to save preferences to account",
saveToAccountNeedLogin: "Please sign in before saving to account",
clearAndLogout: "Clear Cache & Logout",
mode: "Mode",
general: "General",
@@ -64,6 +64,7 @@ export default {
enterpriseSetting: "企业设置",
myProjectManager: "我的项目",
myProjectDetail: "项目详情",
projectDetail: "项目详情",
projectJoin: "加入项目",
currentProject: "当前项目",
projectMemberManager: "项目成员管理",
@@ -31,6 +31,15 @@ export default {
copyPreferences: "复制偏好设置",
copyPreferencesSuccessTitle: "复制成功",
copyPreferencesSuccess: "复制成功,请在 app 下的 `src/preferences.ts`内进行覆盖",
importPreferences: "从剪切板导入",
importPreferencesSuccessTitle: "导入成功",
importPreferencesSuccess: "已从剪切板导入偏好设置",
importPreferencesErrorTitle: "导入失败",
importPreferencesError: "剪切板内容无效,请先复制偏好设置 JSON",
saveToAccount: "保存到账号",
saveToAccountSuccess: "偏好设置已保存到账号",
saveToAccountError: "保存到账号失败",
saveToAccountNeedLogin: "请先登录后再保存到账号",
clearAndLogout: "清空缓存 & 退出登录",
mode: "模式",
general: "通用",
@@ -390,4 +390,10 @@ export const useSettingStore = defineStore({
mitter.on("app.login", async () => {
await useSettingStore().init();
try {
const { loadPreferencesFromAccount } = await import("/@/vben/layouts/widgets/preferences/account-sync");
await loadPreferencesFromAccount();
} catch (e) {
console.error("加载账号偏好设置失败", e);
}
});
@@ -18,6 +18,8 @@ export {
CircleCheckBig,
CircleHelp,
Copy,
ClipboardPaste,
CloudUpload,
CornerDownLeft,
Ellipsis,
Expand,
@@ -0,0 +1,66 @@
import { message as antdMessage } from "ant-design-vue";
import { $t, loadLocaleMessages } from "/@/locales";
import { updatePreferences } from "/@/vben/preferences";
import { PreferencesSettingsGet, PreferencesSettingsSave } from "./api";
const PREFERENCES_KNOWN_KEYS = ["app", "theme", "logo", "sidebar", "header", "tabbar", "breadcrumb", "navigation", "widget", "footer", "copyright", "shortcutKeys", "transition"];
export function isPreferencesPayload(value: unknown): value is Record<string, any> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
return PREFERENCES_KNOWN_KEYS.some(key => Object.prototype.hasOwnProperty.call(value, key));
}
/** 保存到账号:允许空对象(重置后的默认偏好) */
export function isPreferencesSavePayload(value: unknown): value is Record<string, any> {
if (value == null || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const keys = Object.keys(value as Record<string, any>);
if (keys.length === 0) {
return true;
}
return isPreferencesPayload(value);
}
export async function applyPreferencesFromAccount(data: Record<string, any>) {
updatePreferences(data);
if (data.app?.locale) {
await loadLocaleMessages(data.app.locale);
}
}
/**
*
* - null / {}使
*/
export async function loadPreferencesFromAccount() {
const data = await PreferencesSettingsGet();
if (data == null) {
return false;
}
if (typeof data !== "object" || Array.isArray(data)) {
return false;
}
// 保存为空时,后续拉取到空数据后默认从本地读取,不覆盖本地
if (Object.keys(data).length === 0) {
return false;
}
if (!isPreferencesPayload(data)) {
return false;
}
await applyPreferencesFromAccount(data);
return true;
}
export async function savePreferencesToAccount(preferencesPayload: Record<string, any> | null | undefined) {
const payload = preferencesPayload && typeof preferencesPayload === "object" ? preferencesPayload : {};
if (!isPreferencesSavePayload(payload)) {
throw new Error("invalid preferences payload");
}
await PreferencesSettingsSave(payload);
antdMessage.success($t("preferences.saveToAccountSuccess"));
}
@@ -0,0 +1,21 @@
// @ts-ignore
import { request } from "/@/api/service";
const apiPrefix = "/user/settings/preferences";
export async function PreferencesSettingsGet(): Promise<Record<string, any> | null> {
const res = await request({
url: apiPrefix + "/get",
method: "post",
showErrorNotify: false,
});
return (res as Record<string, any>) || null;
}
export async function PreferencesSettingsSave(preferences: Record<string, any>) {
return await request({
url: apiPrefix + "/save",
method: "post",
data: { preferences },
});
}
@@ -16,21 +16,27 @@ import type { SegmentedItem } from "/@/vben//shadcn-ui";
import { computed, ref } from "vue";
import { Copy, RotateCw, X } from "/@/vben/icons";
import { ClipboardPaste, CloudUpload, Copy, RotateCw, X } from "/@/vben/icons";
import { $t, loadLocaleMessages } from "/@/locales";
import { clearPreferencesCache, preferences, resetPreferences, usePreferences } from "/@/vben/preferences";
import { useVbenDrawer } from "/@/vben//popup-ui";
import { VbenButton, VbenIconButton, VbenSegmented } from "/@/vben//shadcn-ui";
import { globalShareState } from "/@/vben//shared/global-state";
import { useUserStore } from "/@/store/user";
import { useClipboard } from "@vueuse/core";
import { Animation, Block, Breadcrumb, BuiltinTheme, ColorMode, Content, Copyright, Footer, General, GlobalShortcutKeys, Header, Layout, Navigation, Radius, Sidebar, Tabbar, Theme, Widget } from "./blocks";
import { applyPreferencesFromAccount, isPreferencesPayload, savePreferencesToAccount } from "./account-sync";
import { message as antdMessage } from "ant-design-vue";
const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
const message = globalShareState.getMessage();
const userStore = useUserStore();
const savingToAccount = ref(false);
const appLocale = defineModel<SupportedLanguagesType>("appLocale");
const appDynamicTitle = defineModel<boolean>("appDynamicTitle");
@@ -150,6 +156,41 @@ async function handleCopy() {
await copy(JSON.stringify(diffPreference.value, null, 2));
message.copyPreferencesSuccess?.($t("preferences.copyPreferencesSuccessTitle"), $t("preferences.copyPreferencesSuccess"));
antdMessage.success($t("preferences.copyPreferencesSuccessTitle"));
}
async function handleImport() {
try {
const text = await navigator.clipboard.readText();
const data = JSON.parse(text);
if (!isPreferencesPayload(data)) {
throw new Error("invalid preferences payload");
}
await applyPreferencesFromAccount(data);
message.copyPreferencesSuccess?.($t("preferences.importPreferencesSuccessTitle"), $t("preferences.importPreferencesSuccess"));
antdMessage.success($t("preferences.importPreferencesSuccess"));
} catch {
antdMessage.error($t("preferences.importPreferencesError"));
}
}
async function handleSaveToAccount() {
if (!userStore.isLogined) {
antdMessage.warning($t("preferences.saveToAccountNeedLogin"));
return;
}
if (savingToAccount.value) {
return;
}
savingToAccount.value = true;
try {
// diff {}使
await savePreferencesToAccount((diffPreference.value as Record<string, any>) || {});
} catch (e: any) {
antdMessage.error(e?.message || $t("preferences.saveToAccountError"));
} finally {
savingToAccount.value = false;
}
}
async function handleClearCache() {
@@ -310,13 +351,25 @@ async function handleReset() {
</div>
<template #footer>
<VbenButton :disabled="!diffPreference" class="mx-4 w-full" size="sm" variant="default" @click="handleCopy">
<div class="flex w-full flex-col gap-2 px-1">
<div class="flex w-full gap-2">
<VbenButton :disabled="!diffPreference" class="w-full" size="sm" variant="default" @click="handleCopy">
<Copy class="mr-2 size-3" />
{{ $t("preferences.copyPreferences") }}
</VbenButton>
<VbenButton :disabled="!diffPreference" class="mr-4 w-full" size="sm" variant="ghost" @click="handleClearCache">
<VbenButton class="w-full" size="sm" variant="outline" @click="handleImport">
<ClipboardPaste class="mr-2 size-3" />
{{ $t("preferences.importPreferences") }}
</VbenButton>
</div>
<VbenButton :disabled="!userStore.isLogined || savingToAccount" class="w-full" size="sm" variant="outline" @click="handleSaveToAccount">
<CloudUpload class="mr-2 size-3" />
{{ $t("preferences.saveToAccount") }}
</VbenButton>
<VbenButton :disabled="!diffPreference" class="w-full" size="sm" variant="ghost" @click="handleClearCache">
{{ $t("preferences.clearAndLogout") }}
</VbenButton>
</div>
</template>
</Drawer>
</div>
@@ -17,7 +17,7 @@
</template>
<script lang="ts">
import { defineComponent, reactive, ref, watch, inject, onMounted } from "vue";
import { defineComponent, reactive, ref, watch, inject, onMounted, Ref } from "vue";
import CertAccessModal from "./access/index.vue";
import { createAccessApi } from "../api";
import { message } from "ant-design-vue";
@@ -64,9 +64,9 @@ export default defineComponent({
setup(props, ctx) {
const api = createAccessApi(props.from);
const target = ref({});
const target:Ref<any> = ref({});
const selectedId = ref();
async function refreshTarget(value) {
async function refreshTarget(value:any) {
selectedId.value = value;
if (value > 0) {
target.value = await api.GetSimpleInfo(value);
@@ -83,7 +83,7 @@ export default defineComponent({
const userStore = useUserStore();
const projectStore = useProjectStore();
async function emitValue(value) {
async function emitValue(value:any) {
const userId = userStore.userInfo.id;
const isEnterprice = projectStore.isEnterprise;
if (pipeline?.value) {
@@ -132,7 +132,7 @@ export default defineComponent({
const providerDefine = ref({});
async function refreshProviderDefine(type) {
async function refreshProviderDefine(type:any) {
providerDefine.value = await api.GetProviderDefine(type);
}
watch(
@@ -0,0 +1,40 @@
/// <reference types="mocha" />
/// <reference types="node" />
import assert from "node:assert/strict";
import { parseUserPreferencesPayload } from "./user-preferences.js";
describe("parseUserPreferencesPayload", () => {
it("parses wrapped preferences payload", () => {
const result = parseUserPreferencesPayload({
preferences: {
theme: { mode: "dark" },
},
});
assert.deepEqual(result, { theme: { mode: "dark" } });
});
it("parses direct preferences payload", () => {
const result = parseUserPreferencesPayload({
app: { locale: "en-US" },
theme: { mode: "light" },
});
assert.deepEqual(result, {
app: { locale: "en-US" },
theme: { mode: "light" },
});
});
it("parses empty preferences as reset payload", () => {
assert.deepEqual(parseUserPreferencesPayload({}), {});
assert.deepEqual(parseUserPreferencesPayload({ preferences: {} }), {});
});
it("returns null for invalid payload", () => {
assert.equal(parseUserPreferencesPayload(null), null);
assert.equal(parseUserPreferencesPayload([]), null);
assert.equal(parseUserPreferencesPayload({ foo: 1 }), null);
assert.equal(parseUserPreferencesPayload({ preferences: { foo: 1 } }), null);
});
});
@@ -0,0 +1,41 @@
const PREFERENCES_KNOWN_KEYS = [
"app",
"theme",
"logo",
"sidebar",
"header",
"tabbar",
"breadcrumb",
"navigation",
"widget",
"footer",
"copyright",
"shortcutKeys",
"transition",
];
/**
* payload
* `{ preferences: {...} }`
* `{}`
*/
export function parseUserPreferencesPayload(value: unknown): Record<string, any> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
const raw = value as Record<string, any>;
const preferences =
raw.preferences != null && typeof raw.preferences === "object" && !Array.isArray(raw.preferences)
? (raw.preferences as Record<string, any>)
: raw;
const preferenceKeys = Object.keys(preferences);
// 空对象表示重置后的默认偏好
if (preferenceKeys.length === 0) {
return {};
}
const hasKnownKey = PREFERENCES_KNOWN_KEYS.some(key => Object.prototype.hasOwnProperty.call(preferences, key));
if (!hasKnownKey) {
return null;
}
return preferences;
}
@@ -2,10 +2,11 @@ import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/c
import { Constants, CrudController } from "@certd/lib-server";
import { UserSettingsService } from "../../../modules/mine/service/user-settings-service.js";
import { UserSettingsEntity } from "../../../modules/mine/entity/user-settings.js";
import { UserGrantSetting } from "../../../modules/mine/service/models.js";
import { UserGrantSetting, UserPreferencesSetting } from "../../../modules/mine/service/models.js";
import { isPlus } from "@certd/plus-core";
import { merge } from "lodash-es";
import { ApiTags } from "@midwayjs/swagger";
import { parseUserPreferencesPayload } from "./user-preferences.js";
/**
*/
@@ -90,4 +91,37 @@ export class UserSettingsController extends CrudController<UserSettingsService>
await this.service.saveSetting(userId, null, setting);
return this.ok({});
}
@Post("/preferences/get", { description: Constants.per.authOnly, summary: "获取用户偏好设置" })
async preferencesGet() {
const userId = this.getUserId();
const entity = await this.service.getByKey(UserPreferencesSetting.__key__, userId, null);
if (!entity?.setting) {
return this.ok(null);
}
let parsed: unknown;
try {
parsed = JSON.parse(entity.setting);
} catch {
return this.ok(null);
}
return this.ok(parseUserPreferencesPayload(parsed));
}
@Post("/preferences/save", { description: Constants.per.authOnly, summary: "保存用户偏好设置" })
async preferencesSave(@Body(ALL) bean: any) {
const userId = this.getUserId();
const preferences = parseUserPreferencesPayload(bean);
if (!preferences) {
throw new Error("偏好设置内容无效");
}
// 整份替换,避免 saveSetting 深合并留下已恢复为默认值的旧字段
const entity = new UserSettingsEntity();
entity.key = UserPreferencesSetting.__key__;
entity.title = UserPreferencesSetting.__title__;
entity.userId = userId;
entity.setting = JSON.stringify({ preferences });
await this.service.save(entity);
return this.ok({});
}
}
@@ -52,6 +52,14 @@ export class UserGrantSetting extends BaseSettings {
allowAdminViewCerts = false;
}
export class UserPreferencesSetting extends BaseSettings {
static __title__ = "用户偏好设置";
static __key__ = "user.preferences";
/** 偏好差异配置(相对默认值),与前端 diffPreference 结构一致 */
preferences: Record<string, any> = {};
}
export class UserDomainImportSetting extends BaseSettings {
static __title__ = "用户域名导入设置";
static __key__ = "user.domain.import";
@@ -144,6 +144,17 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
})
createOnNotFound: boolean;
@TaskInput({
title: "调试模式",
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "是否开启调试模式,开启后将打印更多日志",
})
debug: boolean;
K8sClient: any;
async onInstance() {
const sdk = await import("@certd/lib-k8s");
@@ -157,10 +168,14 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
const kubeConfigStr = await this.getKubeConfig(client, clusterId, isPrivateIpAddress);
this.logger.info("kubeconfig已成功获取");
if (this.debug) {
this.logger.info("kubeconfig:", kubeConfigStr);
}
const k8sClient = new this.K8sClient({
kubeConfigStr,
logger: this.logger,
skipTLSVerify: this.skipTLSVerify,
debug: this.debug,
});
await this.patchCertSecret({ cert, k8sClient });
@@ -173,7 +188,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
}
}
async restartIngress(options: { k8sClient: any }) {
async restartIngress(options: { k8sClient: any; }) {
const { k8sClient } = options;
const { namespace } = this;
@@ -184,6 +199,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
},
},
};
const ingressList = await k8sClient.getIngressList({ namespace });
this.logger.info("ingressList:", ingressList);
if (!ingressList || !ingressList.items) {
@@ -210,7 +226,7 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
}
}
async patchCertSecret(options: { cert: CertInfo; k8sClient: any }) {
async patchCertSecret(options: { cert: CertInfo; k8sClient: any; }) {
const { cert, k8sClient } = options;
const crt = cert.crt;
const key = cert.key;
@@ -266,6 +282,9 @@ export class DeployCertToAliyunAckPlugin extends AbstractTaskPlugin {
try {
const res = await client.request(httpMethod, uriPath, queries, body, headers, requestOption);
if (this.debug) {
this.logger.info("res:", res);
}
return res.config;
} catch (e) {
console.error("请求出错:", e);
@@ -5,6 +5,7 @@ import { IContext } from "@certd/pipeline";
import { IDnsProvider, IDomainParser } from "@certd/plugin-lib";
import punycode from "punycode.js";
import { IOssClient } from "../../../plugin-lib/index.js";
import { NonRetryableException } from "@certd/lib-server";
export type CnameVerifyPlan = {
type?: string;
domain: string;
@@ -531,6 +532,7 @@ export class AcmeService {
};
/* 自动申请证书 */
const challengePriority = domainsVerifyPlan && Object.values(domainsVerifyPlan).some((item: any) => item?.type === "dns-persist") ? ["dns-persist-01"] : ["dns-01", "http-01"];
try {
const crt = await client.auto({
csr,
email: email,
@@ -563,6 +565,14 @@ export class AcmeService {
this.logger.debug(`Certificate:\n${cert.crt}`);
this.logger.info("证书申请成功");
return cert;
} catch (e) {
const message = e?.message;
const REDUNDANT_WILDCARD_DOMAIN_ERROR = "redundant with a wildcard domain in the same request";
if (message != null && message.indexOf(REDUNDANT_WILDCARD_DOMAIN_ERROR) >= 0) {
throw new NonRetryableException(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
}
throw e;
}
}
buildCommonNameByDomains(domains: string | string[]): {
@@ -1,4 +1,6 @@
import assert from "assert";
import { utils } from "@certd/basic";
import { NonRetryableException } from "@certd/lib-server";
import { CertApplyPlugin } from "./apply.js";
describe("CertApplyPlugin dns-persist verify plan", () => {
@@ -45,3 +47,114 @@ describe("CertApplyPlugin dns-persist verify plan", () => {
assert.equal(plan["handfree.work"].dnsPersistVerifyPlan?.recordValue, "letsencrypt.org; accounturi=https://acme.example/acct/1; policy=wildcard");
});
});
describe("CertApplyPlugin certificate apply retry", () => {
it("does not retry by default", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const error = new Error("apply failed");
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw error;
},
};
await assert.rejects(plugin.orderWithRetry({}), error);
assert.equal(orderCount, 1);
assert.equal(plugin.certApplyRetryCount, 0);
});
it("retries after a 30-second cooldown and succeeds before reaching the limit", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const waitTimes: number[] = [];
plugin.certApplyRetryCount = 2;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
if (orderCount < 3) {
throw new Error(`apply failed ${orderCount}`);
}
return { crt: "certificate", key: "private-key" };
},
};
const originalSleep = utils.sleep;
utils.sleep = async (waitTime: number) => {
waitTimes.push(waitTime);
};
try {
const cert = await plugin.orderWithRetry({});
assert.deepEqual(cert, { crt: "certificate", key: "private-key" });
assert.equal(orderCount, 3);
assert.deepEqual(waitTimes, [30_000, 30_000]);
} finally {
utils.sleep = originalSleep;
}
});
it("throws the last error after reaching the retry limit", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const error = new Error("apply failed");
plugin.certApplyRetryCount = 1;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw error;
},
};
const originalSleep = utils.sleep;
utils.sleep = async () => {};
try {
await assert.rejects(plugin.orderWithRetry({}), error);
assert.equal(orderCount, 2);
} finally {
utils.sleep = originalSleep;
}
});
it("does not retry a cancelled apply", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const error: any = new Error("cancelled");
error.name = "CancelError";
plugin.certApplyRetryCount = 2;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw error;
},
};
await assert.rejects(plugin.orderWithRetry({}), error);
assert.equal(orderCount, 1);
});
it("throws a non-retryable error for wildcard and normal domain conflicts", async () => {
const plugin: any = new CertApplyPlugin();
let orderCount = 0;
const message = "example.com is redundant with a wildcard domain in the same request";
plugin.certApplyRetryCount = 2;
plugin.logger = { warn() {} };
plugin.acme = {
async order() {
orderCount++;
throw new NonRetryableException(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
},
};
await assert.rejects(plugin.orderWithRetry({}), (error: any) => {
assert.equal(error instanceof NonRetryableException, true);
assert.equal(error.message, `通配符域名已经包含了普通域名,请删除其中一个(${message}`);
return true;
});
assert.equal(orderCount, 1);
});
});
@@ -1,5 +1,6 @@
import { CancelError, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { utils } from "@certd/basic";
import { NonRetryableException } from "@certd/lib-server";
import { AcmeAccountInfo, AcmeService, DomainsVerifyPlan, DomainVerifyPlan, PrivateKeyType, SSLProvider } from "./acme.js";
import { createDnsProvider, DnsProviderContext, DnsVerifier, DomainVerifiers, HttpVerifier, IDnsProvider, IDomainVerifierGetter, ISubDomainsGetter } from "@certd/plugin-lib";
@@ -61,6 +62,7 @@ const preferredChainConfigs = {
} as const;
const preferredChainSupportedProviders = Object.keys(preferredChainConfigs);
const CERT_APPLY_RETRY_DELAY_MS = 30_000;
const preferredChainMergeScript = (() => {
const configs = JSON.stringify(preferredChainConfigs);
@@ -551,6 +553,20 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
})
waitDnsDiffuseTime = 30;
@TaskInput({
title: "证书申请失败重试次数",
value: 0,
component: {
name: "a-input-number",
vModel: "value",
min: 0,
step: 1,
},
maybeNeed: true,
helper: "证书申请失败后,等待30秒再自动重试;0表示不重试",
})
certApplyRetryCount = 0;
acme!: AcmeService;
eab!: EabAccess;
@@ -651,8 +667,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
dnsProvider = await this.createDnsProvider(dnsProviderType, access);
}
try {
const cert = await this.acme.order({
const cert = await this.orderWithRetry({
email,
domains,
dnsProvider,
@@ -666,19 +681,39 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
const certInfo = this.formatCerts(cert);
return new CertReader(certInfo);
}
private async orderWithRetry(orderOptions: Parameters<AcmeService["order"]>[0]) {
const maxRetryCount = this.getCertApplyRetryCount();
let retryCount = 0;
while (true) {
try {
return await this.acme.order(orderOptions);
} catch (e: any) {
const message: string = e?.message;
if (message != null && message.indexOf("redundant with a wildcard domain in the same request") >= 0) {
this.logger.error(e);
throw new Error(`通配符域名已经包含了普通域名,请删除其中一个(${message}`);
}
if (e.name === "CancelError") {
throw new CancelError(e.message);
}
if (e instanceof NonRetryableException) {
throw e;
}
if (e?.name === "CancelError" || retryCount >= maxRetryCount) {
throw e;
}
retryCount++;
this.logger.warn(`证书申请失败,等待30秒后重试(${retryCount}/${maxRetryCount}`, e);
await utils.sleep(CERT_APPLY_RETRY_DELAY_MS);
}
}
}
private getCertApplyRetryCount() {
const retryCount = Number(this.certApplyRetryCount);
if (!Number.isFinite(retryCount) || retryCount <= 0) {
return 0;
}
return Math.floor(retryCount);
}
async createDnsProvider(dnsProviderType: string, dnsProviderAccess: any): Promise<IDnsProvider> {
const domainParser = this.acme.options.domainParser;
const context: DnsProviderContext = {
@@ -76,7 +76,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
this.clearLastStatus();
if (this.successNotify) {
await this.sendSuccessNotify();
await this.sendSuccessNotify(cert);
}
} else {
throw new Error("申请证书失败");
@@ -165,12 +165,14 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
nextUpdateDays: leftDays - maxDays,
};
}
async sendSuccessNotify() {
async sendSuccessNotify(certReader: CertReader) {
this.logger.info("发送证书申请成功通知");
const url = await this.ctx.urlService.getPipelineDetailUrl(this.pipeline.id, this.ctx.runtime.id);
const body: NotificationBody = {
title: `证书申请成功【${this.pipeline.title}`,
content: `域名:${this.domains.join(",")}`,
content: `域名:${this.domains.join(",")}\n
${dayjs(certReader.expires).format("YYYY-MM-DD HH:mm:ss")}\n
`,
url: url,
notificationType: "certApplySuccess",
};
@@ -53,6 +53,22 @@ export class JDCloudDeployToCDN extends AbstractTaskPlugin {
)
domainName!: string | string[];
@TaskInput({
title: "跳转类型",
helper: "http与https之间的跳转方式,default:不强制跳转",
value: "default",
component: {
name: "a-select",
options: [
{ label: "默认", value: "default" },
{ label: "强制跳转http", value: "http" },
{ label: "强制跳转https", value: "https" },
],
},
required: false,
})
jumpType;
async onInstance() {}
async execute(): Promise<void> {
@@ -101,7 +117,7 @@ export class JDCloudDeployToCDN extends AbstractTaskPlugin {
httpType: "https",
// certificate: certInfo.crt,
// rsaKey: certInfo.key,
jumpType: "default",
jumpType: this.jumpType || "default", // 旧版数据未配置跳转类型时,走默认跳转
certFrom: "ssl",
sslCertId: certId, // 不用certId 方式,会报证书已存在错误,目前还没找到怎么查询重复证书
syncToSsl: false,
@@ -1,15 +1,17 @@
import crypto from "node:crypto";
import { BaotaAccess } from "../access.js";
import { HttpClient, HttpRequestConfig } from "@certd/basic";
import { HttpClient, HttpRequestConfig, ILogger } from "@certd/basic";
import * as querystring from "node:querystring";
export class BaotaClient {
access: BaotaAccess;
http: HttpClient;
logger: ILogger
constructor(access: BaotaAccess, http: HttpClient) {
this.access = access;
this.http = http;
this.logger = access.ctx.logger;
}
//将以上 java代码 翻译成nodejs 代码
@@ -104,12 +104,15 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
}
const lockKey = `baota-lock-${accessId}`;
if (this.isDockerSite) {
this.logger.info(`当前已勾选docker站点(如果部署失败,请确认站点:${siteNames} 是否全部为docker站点)`);
}
for (const site of siteNames) {
// 加锁,防止并发部署证书, 宝塔并发部署会导致nginx的conf错乱
await this.ctx.utils.locker.execute(lockKey, async () => {
this.logger.info(`为站点:${site}设置证书,目前支持宝塔网站站点、docker站点`);
try {
if (this.isDockerSite) {
this.logger.info(`为Docker站点:${site} 设置证书`);
const res = await client.doRequest("/mod/docker/com/set_ssl", "", {
site_name: site,
key: cert.key,
@@ -117,6 +120,7 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
});
this.logger.info(res?.msg);
} else {
this.logger.info(`为非Docker站点:${site} 设置证书`);
const res = await client.doRequest("/site", "SetSSL", {
type: 0,
siteName: site,
@@ -125,6 +129,13 @@ export class BaotaDeployWebSiteCert extends AbstractTaskPlugin {
});
this.logger.info(res?.msg);
}
} catch (e: any) {
if (e?.message?.includes("没有服务器配置文件")) {
this.logger.error(e.message);
this.logger.warn(`首先请确认站点 ${site} 是否存在,如果存在,请到宝塔上手动保存一下该站点证书试试`);
}
throw e
}
});
}
@@ -9,7 +9,7 @@ import { TencentAccess, TencentSslClient } from "../../../plugin-lib/tencent/ind
title: "腾讯云-删除即将过期证书",
icon: "svg:icon-tencentcloud",
group: pluginGroups.tencent.key,
desc: "仅删除未使用的证书",
desc: "仅删除即将过期且未使用的证书",
dependPlugins: {
"access:tencent": "*",
},
@@ -0,0 +1,111 @@
/// <reference types="mocha" />
import assert from "node:assert/strict";
import { DeployCertToTencentCLB } from "./index.js";
describe("DeployCertToTencentCLB", () => {
it("uses remote single-select inputs for CLB and HTTPS listener", () => {
const input = (DeployCertToTencentCLB as any).define.input;
assert.equal(input.loadBalancerId.component.name, "remote-select");
assert.equal(input.loadBalancerId.component.single, true);
assert.equal(input.loadBalancerId.component.action, "onGetCLBList");
assert.equal(input.loadBalancerId.required, true);
assert.equal(input.listenerId.component.name, "remote-select");
assert.equal(input.listenerId.component.single, true);
assert.equal(input.listenerId.component.action, "onGetListenerList");
assert.deepEqual(input.listenerId.component.watches, ["certDomains", "accessId", "region", "loadBalancerId"]);
assert.equal(input.listenerId.required, true);
assert.equal(input.domain.component.name, "remote-select");
assert.equal(input.domain.component.single, false);
assert.equal(input.domain.component.action, "onGetDomainList");
assert.deepEqual(input.domain.component.watches, ["certDomains", "accessId", "region", "loadBalancerId", "listenerId"]);
assert.equal(input.domain.required, false);
});
it("maps CLB API results to remote-select options", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getCLBList = async () => [
{
LoadBalancerId: "lb-1",
LoadBalancerName: "业务负载均衡",
},
];
const options = await plugin.onGetCLBList({});
assert.deepEqual(options, [
{
value: "lb-1",
label: "业务负载均衡<lb-1>",
},
]);
});
it("maps HTTPS listener API results after selecting a CLB", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.loadBalancerId = "lb-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: any) => {
assert.equal(loadBalancerId, "lb-1");
assert.equal(listenerIds, null);
return [
{
ListenerId: "listener-1",
ListenerName: "HTTPS监听器",
Port: 443,
},
];
};
const options = await plugin.onGetListenerList({});
assert.deepEqual(options, [
{
value: "listener-1",
label: "HTTPS监听器:443<listener-1>",
},
]);
});
it("maps SNI domains from the selected listener rules", async () => {
const plugin = new DeployCertToTencentCLB();
plugin.accessId = "access-1";
plugin.loadBalancerId = "lb-1";
plugin.listenerId = "listener-1";
plugin.logger = { info: () => undefined } as any;
(plugin as any).getClient = async () => ({});
(plugin as any).getListenerList = async (_client: any, loadBalancerId: string, listenerIds: string[]) => {
assert.equal(loadBalancerId, "lb-1");
assert.deepEqual(listenerIds, ["listener-1"]);
return [
{
ListenerId: "listener-1",
Rules: [{ Domain: "www.example.com" }, { Domain: "api.example.com" }],
},
];
};
const options = await plugin.onGetDomainList({});
assert.deepEqual(options, [
{
value: "www.example.com",
label: "www.example.com",
domain: "www.example.com",
},
{
value: "api.example.com",
label: "api.example.com",
domain: "api.example.com",
},
]);
});
});
@@ -1,7 +1,8 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import dayjs from "dayjs";
import { TencentAccess } from "../../../plugin-lib/tencent/index.js";
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
@IsTaskPlugin({
name: "DeployCertToTencentCLB",
title: "腾讯云-部署到CLB",
@@ -73,29 +74,44 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
})
region!: string;
@TaskInput({
@TaskInput(
createRemoteSelectInputDefine({
title: "负载均衡ID",
required: true,
helper: "请选择要部署证书的负载均衡",
action: DeployCertToTencentCLB.prototype.onGetCLBList.name,
watches: ["region"],
single: true,
pager: false,
search: false,
})
)
loadBalancerId!: string;
@TaskInput({
@TaskInput(
createRemoteSelectInputDefine({
title: "监听器ID",
required: true,
helper: "请选择要部署证书的HTTPS监听器",
action: DeployCertToTencentCLB.prototype.onGetListenerList.name,
watches: ["region", "loadBalancerId"],
single: true,
pager: false,
search: false,
})
)
listenerId!: string;
@TaskInput({
@TaskInput(
createRemoteSelectInputDefine({
title: "域名",
helper: "如果开启了SNI,请选择要部署证书的域名;未开启SNI时可以留空",
action: DeployCertToTencentCLB.prototype.onGetDomainList.name,
watches: ["region", "loadBalancerId", "listenerId"],
required: false,
component: {
name: "a-select",
vModel: "value",
open: false,
mode: "tags",
},
helper: "如果开启了sni,则此项必须填写,未开启,则不要填写",
single: false,
pager: false,
search: false,
})
)
domain!: string | string[];
@TaskInput({
@@ -272,6 +288,27 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return ret.LoadBalancerSet;
}
async onGetCLBList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
const client = await this.getClient();
const list = await this.getCLBList(client);
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
const loadBalancerId = item.LoadBalancerId;
const loadBalancerName = item.LoadBalancerName || loadBalancerId;
return {
value: loadBalancerId,
label: `${loadBalancerName}<${loadBalancerId}>`,
};
});
}
async getListenerList(client: any, balancerId: any, listenerIds: any) {
// HTTPS
const params = {
@@ -284,6 +321,57 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
return ret.Listeners;
}
async onGetListenerList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
if (!this.loadBalancerId) {
throw new Error("请先选择负载均衡");
}
const client = await this.getClient();
const list = await this.getListenerList(client, this.loadBalancerId, null);
if (!list || list.length === 0) {
return [];
}
return list.map((item: any) => {
const listenerId = item.ListenerId;
const listenerName = item.ListenerName || "HTTPS监听器";
const port = item.Port ? `:${item.Port}` : "";
return {
value: listenerId,
label: `${listenerName}${port}<${listenerId}>`,
};
});
}
async onGetDomainList(data: PageSearch) {
if (!this.accessId) {
throw new Error("请选择Access提供者");
}
if (!this.loadBalancerId) {
throw new Error("请先选择负载均衡");
}
if (!this.listenerId) {
throw new Error("请先选择监听器");
}
const client = await this.getClient();
const listeners = await this.getListenerList(client, this.loadBalancerId, [this.listenerId]);
const listener = listeners?.[0];
const domains = listener?.Rules?.map((rule: any) => rule.Domain).filter(Boolean) || [];
const uniqueDomains = [...new Set(domains)];
return uniqueDomains.map(domain => {
return {
value: domain,
label: domain,
domain,
};
});
}
checkRet(ret: any) {
if (!ret || ret.Error) {
throw new Error("执行失败:" + ret.Error.Code + "," + ret.Error.Message);
@@ -103,6 +103,9 @@ export class VolcengineDeployToDCDN extends AbstractTaskPlugin {
this.certDomains = new CertReader(this.cert).getAllDomains();
let domainList = this.domainList;
if (typeof domainList === "string") {
domainList = [domainList];
}
if (!this.autoMatch) {
//手动根据域名部署
if (!this.domainList || this.domainList.length === 0) {
@@ -128,21 +131,20 @@ export class VolcengineDeployToDCDN extends AbstractTaskPlugin {
}
}
//域名十个十个的分割
for (let i = 0; i < domainList.length; i += 10) {
const batch = domainList.slice(i, i + 10);
this.logger.info(`开始部署证书到域名:${batch}`);
for (let i = 0; i < domainList.length; i++) {
this.logger.info(`开始部署证书到域名:${domainList[i]}`);
const res = await service.request({
action: "CreateCertBind",
method: "POST",
body: {
DomainNames: batch,
DomainNames: [domainList[i]],
CertSource: "volc",
CertId: certId,
},
version: "2021-04-01",
});
this.logger.info(`部署证书到域名成功:`, JSON.stringify(res));
await this.ctx.utils.sleep(2000);
}
this.logger.info("部署完成");
+1 -1
View File
@@ -1 +1 @@
23:35
01:14
+1 -1
View File
@@ -1 +1 @@
23:54
01:28