Compare commits

...

24 Commits

Author SHA1 Message Date
xiaojunnuo
83df29d832 chore: 1 2026-01-22 11:10:53 +08:00
xiaojunnuo
607afe864a perf: cname记录支持批量导入和导出 2026-01-22 10:56:45 +08:00
xiaojunnuo
a97cee84f3 perf: 支持同步域名过期时间 2026-01-22 00:59:28 +08:00
xiaojunnuo
ad64384891 perf: 域名导入 2026-01-21 18:24:03 +08:00
xiaojunnuo
f75c73d739 perf: 优化流水线创建入口,各种证书申请任务类型拆分成多个按钮 2026-01-21 13:27:14 +08:00
xiaojunnuo
418bcddc95 fix: 修复流水线复制出错的bug 2026-01-20 16:56:36 +08:00
xiaojunnuo
61192b998a fix: 修复插件修改名字和删除后没有注销注册的bug 2026-01-20 11:52:14 +08:00
xiaojunnuo
5ea2b09dc3 fix: 编辑插件author不允许出现符号 2026-01-20 11:18:10 +08:00
xiaojunnuo
5bfc2c4a9b chore: 1 2026-01-20 00:15:31 +08:00
xiaojunnuo
8ec47c3894 Merge branch 'v2-domain-sync' of https://github.com/certd/certd into v2-domain-sync 2026-01-20 00:13:10 +08:00
xiaojunnuo
f4423638a2 perf: 支持从提供商导入域名列表 2026-01-20 00:13:05 +08:00
xiaojunnuo
7b3444308b chore: docs 2026-01-19 16:27:34 +08:00
xiaojunnuo
5ec9916817 chore: FormDialog 2026-01-19 11:01:48 +08:00
xiaojunnuo
be1a70299f chore: 域名自动同步初步 2026-01-16 18:18:39 +08:00
xiaojunnuo
8685aa371a chore: publish github 2026-01-16 15:47:25 +08:00
xiaojunnuo
0224faa184 chore: publish github 2026-01-16 12:57:14 +08:00
xiaojunnuo
8546e326cf build: update github actions 2026-01-16 09:30:47 +08:00
xiaojunnuo
9956fd2f04 Merge branch 'v2-dev' of https://github.com/certd/certd into v2-dev 2026-01-16 09:12:51 +08:00
xiaojunnuo
4f669ca82f build: release 2026-01-16 01:32:11 +08:00
xiaojunnuo
1cd3881aa8 chore: docs 2026-01-16 01:29:50 +08:00
xiaojunnuo
e634513f7b chore: docs 2026-01-16 01:22:51 +08:00
xiaojunnuo
7b6cde6ae3 build: publish 2026-01-16 01:00:16 +08:00
xiaojunnuo
18146fdf9e build: trigger build image 2026-01-16 01:00:03 +08:00
xiaojunnuo
187d04e3a1 chore: docs 2026-01-15 11:19:26 +08:00
85 changed files with 1313 additions and 303 deletions

View File

@@ -1,45 +0,0 @@
name: build-node-base-image
# 废弃,比默认的占用内存更多
on:
push:
branches: ['v2-dev1']
paths:
- "scripts/build/Dockerfile"
# schedule:
# - # 国际时间 19:17 执行北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
packages: write
jobs:
build-node-base-image:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.dockerhub_username }}
password: ${{ secrets.dockerhub_password }}
- name: Build default platforms
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
context: ./scripts/build/
tags: |
greper/node-base:22-alpine-2

View File

@@ -19,6 +19,7 @@ permissions:
jobs:
deploy-certd-demo:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4

View File

@@ -19,6 +19,7 @@ permissions:
jobs:
publish-atomgit:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4

View File

@@ -17,8 +17,9 @@ permissions:
packages: write
jobs:
publish-atomgit:
publish-gitee:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4

39
.github/workflows/publish-github.yaml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: publish-github
on:
push:
branches: ['v2-dev']
paths:
- "trigger/publish.trigger"
workflow_run:
workflows: [ "build-image-for-release" ]
types:
- completed
# schedule:
# - # 国际时间 19:17 执行北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
packages: write
jobs:
publish-github:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: publish_to_github
id: publish_to_github
run: |
export GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
rm -rf ./pnpm*.yaml
npm install -g pnpm
pnpm install
npm run publish_to_github
working-directory: ./

View File

@@ -108,12 +108,12 @@ export default defineConfig({
text: "常见问题",
items: [
{text: "QA", link: "/guide/qa/use.md"},
{text: "忘记密码/无法登录", link: "/guide/use/forgotpasswd/"},
{text: "群晖证书部署", link: "/guide/use/synology/"},
{text: "腾讯云密钥获取", link: "/guide/use/tencent/"},
{text: "连接windows主机", link: "/guide/use/host/windows.md"},
{text: "Google EAB获取", link: "/guide/use/google/"},
{text: "阿里云相关", link: "/guide/use/aliyun/"},
{text: "忘记密码", link: "/guide/use/forgotpasswd/"},
{text: "数据备份", link: "/guide/use/backup/"},
{text: "Certd本身的证书更新", link: "/guide/use/https/index.md"},
{text: "js脚本插件使用", link: "/guide/use/custom-script/index.md"},
@@ -124,6 +124,7 @@ export default defineConfig({
{text: "子域名托管", link: "/guide/use/cert/subdomain.md"},
{text: "流水线有效期", link: "/guide/use/pipeline/valid.md"},
{text: "IP证书申请", link: "/guide/use/cert/ip.md"},
{text: "插件开发", link: "/guide/use/dev/plugin.md"},
]
},
{

View File

@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.1](https://github.com/certd/certd/compare/v1.38.0...v1.38.1) (2026-01-15)
### Bug Fixes
* 修复自定义插件name丢失author导致找不到插件的bug ([2fbb58e](https://github.com/certd/certd/commit/2fbb58eb2b239eab4864f90aa72b0ef2ada38e8f))
### Performance Improvements
* 优化内存占用 ([4fc8acc](https://github.com/certd/certd/commit/4fc8acce8c1beec38c24b0977b71ff6b18cb52c9))
* 自定义插件支持使用_ctx.import("/@/xxx.js")以绝对路径引用模块 ([9eace86](https://github.com/certd/certd/commit/9eace86aeeb48c23b55102fc5d42088294d9eb97))
# [1.38.0](https://github.com/certd/certd/compare/v1.37.17...v1.38.0) (2026-01-13)
### Bug Fixes

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,19 @@
# 插件开发
## 插件创建
点击自定义插件按钮,填写插件基本信息
![plugin-create.png](images/plugin-create.png)
创建成功后,会默认打开插件编辑页面,里面默认带有示例代码说明,可以在此基础上进行你的自定义开发
![plugin-edit.png](images/plugin-edit.png)
## 插件测试
在流水线中添加插件任务
![plugin-test.png](images/plugin-test1.png)
配置插件任务参数
![plugin-test.png](images/plugin-test2.png)
点击运行,查看插件任务运行结果
![plugin-test.png](images/plugin-test3.png)

View File

@@ -1,7 +1,15 @@
# 忘记管理员密码
# 忘记密码/无法登录
无法登录的情况:
1、忘记管理员密码
2、仅有第三方登录但第三方登录失效导致无法登录
请查看如下方法恢复的登录
## 一、忘记管理员密码
解决方法如下:
## 1. 修改环境变量
### 1. 修改环境变量
docker部署的
修改docker-compose.yaml文件将环境变量`certd_system_resetAdminPasswd`改为`true`
@@ -18,21 +26,28 @@ services:
certd_system_resetAdminPasswd=true
```
## 2. 重启容器
### 2. 重启容器
```shell
docker compose up -d
docker logs -f --tail 500 certd
# 观察日志当日志中输出“重置1号管理员用户密码完成”即可操作下一步
# 这里会打印1号管理员记录的用户名如果你修改过管理员用户名请注意查看此条日志
```
## 3. 恢复环境变量
### 3. 恢复环境变量
修改docker-compose.yaml`certd_system_resetAdminPasswd`改回`false`
## 4. 再次重启容器
### 4. 再次重启容器
```shell
docker compose up -d
```
## 5. 默认密码登录
### 5. 默认密码登录
使用`原管理员账号/123456`登录系统,请及时修改管理员密码
> 默认管理员账号: admin
> 如果忘记管理员账号,请查看修改密码时的启动日志,会打印管理员账号名
## 二、仅有第三方登录,没有登录窗口
当开启仅使用第三方登录模式时,如果第三方登录未配置或已失效,则会导致无法登录
您可以通过访问 `http://你的certd地址/#/login?oauthOnly=false` 来临时关闭仅使用第三方登录模式,以使用密码登录。

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -0,0 +1,2 @@
# 第三方登录配置

View File

@@ -0,0 +1,11 @@
# 用户有效期功能
可以为用户设置有效期,超过有效期后,用户的流水线将停止运行
## 开启用户有效期功能
![开启用户有效期功能](images/user_valid_enable.png)
## 设置用户有效期
![设置用户有效期](images/user_valid_set.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -67,4 +67,31 @@
![](./images/deploy4.png)
## 6. 配置通知和自动运行
![](./images/notify.png)
![](./images/notify.png)
## 三、 常见问题
### 1. 登录超时 status:ECONNABORTED
如果您的certd部署在群晖里面可能会遇到登录超时的问题
```
httpRequest:https://dms.xxxxx.com:5001/webapi/entry.cgi, method:get
请求出错: status:ECONNABORTED, statusText:ECONNABORTED
Axio:sError: timeout of 120000ms exceeded
```
可能的原因是是您的dsm域名指向的ip地址在容器内无法访问导致登录超时
您可以通过配置域名映射来解决
1. 获取群晖dsm内部地址
进入certd后台->系统管理->网络测试, 一般会看到 `172.xx.0.2` 记住这个xx是多少
![](./images/nettest.png)
2. 修改容器编排 docker-compose.yaml
```
services:
certd:
...
extra_hosts: # 放开这段注释
- "你的dsm域名地址:172.xx.0.1" # 将xx替换成上面记住的数字
```

View File

@@ -18,7 +18,7 @@
"start:server": "cd ./packages/ui/certd-server && npm start",
"devb": "lerna run dev-build",
"i-all": "lerna link && lerna exec npm install ",
"publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits --create-release github && npm run afterpublishOnly ",
"publish": "npm run prepublishOnly2 && lerna publish --force-publish=pro/plus-core --conventional-commits && npm run afterpublishOnly ",
"afterpublishOnly": "npm run copylogs && time /t >trigger/build.trigger && git add ./trigger/build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && npm run commitAll",
"transform-sql": "cd ./packages/ui/certd-server/db/ && node --experimental-json-modules transform.js",
"plugin-doc-gen": "cd ./packages/ui/certd-server/ && npm run export-metadata",
@@ -40,6 +40,7 @@
"release": "time /t >trigger/release.trigger && git add trigger/release.trigger && git commit -m \"build: release\" && git push",
"publish_to_atomgit": "node --experimental-json-modules ./scripts/publish-atomgit.js",
"publish_to_gitee": "node --experimental-json-modules ./scripts/publish-gitee.js",
"publish_to_github": "node --experimental-json-modules ./scripts/publish-github.js",
"get_version": "node --experimental-json-modules ./scripts/version.js"
},
"license": "AGPL-3.0",

View File

@@ -70,5 +70,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -47,5 +47,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -11,11 +11,11 @@ export type PageSearch = {
// sortOrder?: "asc" | "desc";
};
export type PageRes = {
export type PageRes<T = any> = {
pageNo?: number;
pageSize?: number;
total?: string;
list: any[];
total?: number;
list: T[];
};
export class Pager {
@@ -34,3 +34,29 @@ export class Pager {
this.pageNo = Math.ceil(offset / (this.pageSize ?? 50)) + 1;
}
}
export async function doPageTurn<T>(req: { pager: Pager; getPage: (pager: Pager) => Promise<PageRes<T>>; itemHandle?: (item: T) => Promise<void>; batchHandle?: (pageRes: PageRes<T>) => Promise<void> }) {
let count = 0;
const { pager, getPage, itemHandle, batchHandle } = req;
while (true) {
const pageRes = await getPage(pager);
if (!pageRes || !pageRes.list || pageRes.list.length === 0) {
break;
}
count += pageRes.list.length;
if (batchHandle) {
await batchHandle(pageRes);
}
if (itemHandle) {
for (const item of pageRes.list) {
await itemHandle(item);
}
}
if (pageRes.total && pageRes.total >= 0 && count >= pageRes.total) {
//遍历完成
break;
}
pager.pageNo++;
}
return count;
}

View File

@@ -22,4 +22,15 @@ const onRegister = ({ key, value }: OnRegisterContext<AbstractTaskPlugin>) => {
}
pluginGroups.other.plugins.push(value.define);
};
export const pluginRegistry = createRegistry<AbstractTaskPlugin>("plugin", onRegister);
const onUnRegister = ({ key }: OnRegisterContext<AbstractTaskPlugin>) => {
for (const group of Object.values(pluginGroups)) {
const index = group.plugins.findIndex(plugin => plugin.name === key);
if (index > -1) {
group.plugins.splice(index, 1);
return;
}
}
};
export const pluginRegistry = createRegistry<AbstractTaskPlugin>("plugin", onRegister, onUnRegister);

View File

@@ -27,10 +27,12 @@ export class Registry<T = any> {
} = {};
onRegister?: OnRegister<T>;
onUnRegister?: OnRegister<T>;
constructor(type: string, onRegister?: OnRegister<T>) {
constructor(type: string, onRegister?: OnRegister<T>, onUnRegister?: OnRegister<T>) {
this.type = type;
this.onRegister = onRegister;
this.onUnRegister = onUnRegister;
}
register(key: string, value: RegistryItem<T>) {
@@ -49,6 +51,13 @@ export class Registry<T = any> {
}
unRegister(key: string) {
if (this.onUnRegister) {
this.onUnRegister({
registry: this,
key,
value: this.storage[key],
});
}
delete this.storage[key];
logger.info(`反注册插件:${this.type}:${key}`);
}
@@ -108,7 +117,7 @@ export class Registry<T = any> {
}
}
export function createRegistry<T>(type: string, onRegister?: OnRegister<T>): Registry<T> {
export function createRegistry<T>(type: string, onRegister?: OnRegister<T>, onUnRegister?: OnRegister<T>): Registry<T> {
const pipelineregistrycacheKey = "PIPELINE_REGISTRY_CACHE";
// @ts-ignore
let cached: any = global[pipelineregistrycacheKey];
@@ -121,7 +130,7 @@ export function createRegistry<T>(type: string, onRegister?: OnRegister<T>): Reg
if (cached[type]) {
return cached[type];
}
const newRegistry = new Registry<T>(type, onRegister);
const newRegistry = new Registry<T>(type, onRegister, onUnRegister);
cached[type] = newRegistry;
return newRegistry;
}

View File

@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -56,5 +56,5 @@
"fetch"
]
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -64,5 +64,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -57,5 +57,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
}

View File

@@ -1,5 +1,5 @@
import { HttpClient, ILogger, utils } from "@certd/basic";
import { IAccess, IServiceGetter, Registrable } from "@certd/pipeline";
import { IAccess, IServiceGetter, Pager, PageRes, Registrable } from "@certd/pipeline";
export type DnsProviderDefine = Registrable & {
accessType: string;
@@ -28,6 +28,11 @@ export type DnsProviderContext = {
serviceGetter: IServiceGetter;
};
export type DomainRecord = {
id: string;
domain: string;
};
export interface IDnsProvider<T = any> {
onInstance(): Promise<void>;
@@ -51,6 +56,8 @@ export interface IDnsProvider<T = any> {
//中文域名是否需要punycode转码如果返回True则使用punycode来添加解析记录否则使用中文域名添加解析记录
usePunyCode(): boolean;
getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>>;
}
export interface ISubDomainsGetter {

View File

@@ -1,4 +1,5 @@
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { Pager, PageRes } from "@certd/pipeline";
import { CreateRecordOptions, DnsProviderContext, DnsProviderDefine, DomainRecord, IDnsProvider, RemoveRecordOptions } from "./api.js";
import { dnsProviderRegistry } from "./registry.js";
import { HttpClient, ILogger } from "@certd/basic";
import punycode from "punycode.js";
@@ -44,6 +45,10 @@ export abstract class AbstractDnsProvider<T = any> implements IDnsProvider<T> {
abstract onInstance(): Promise<void>;
abstract removeRecord(options: RemoveRecordOptions<T>): Promise<void>;
async getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>> {
throw new Error("Method not implemented.");
}
}
export async function createDnsProvider(opts: { dnsProviderType: string; context: DnsProviderContext }): Promise<IDnsProvider> {

View File

@@ -4,6 +4,14 @@ import psl from "psl";
import { ILogger, utils, logger as globalLogger } from "@certd/basic";
import { resolveDomainBySoaRecord } from "@certd/acme-client";
export function parseDomainByPsl(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed;
}
export class DomainParser implements IDomainParser {
subDomainsGetter: ISubDomainsGetter;
logger: ILogger;
@@ -13,11 +21,7 @@ export class DomainParser implements IDomainParser {
}
parseDomainByPsl(fullDomain: string) {
const parsed = psl.parse(fullDomain) as psl.ParsedDomain;
if (parsed.error) {
throw new Error(`解析${fullDomain}域名失败:` + JSON.stringify(parsed.error));
}
return parsed.domain as string;
return parseDomainByPsl(fullDomain).domain as string;
}
async parse(fullDomain: string) {

View File

@@ -3,7 +3,7 @@ VITE_APP_API=api
VITE_APP_PM_ENABLED=true
VITE_APP_TITLE=Certd
VITE_APP_SLOGAN=让你的证书永不过期
VITE_APP_COPYRIGHT_YEAR=2021-2025
VITE_APP_COPYRIGHT_YEAR=2021-2026
VITE_APP_COPYRIGHT_NAME=handsfree.work
VITE_APP_COPYRIGHT_URL=https://certd.handsfree.work
VITE_APP_LOGO=static/images/logo/logo.svg

View File

@@ -1,5 +1,5 @@
<template>
<a-select>
<a-select :value="value" @update:value="onChange">
<a-select-option v-for="item of options" :key="item.value" :value="item.value" :label="item.label">
<span class="flex-o">
<fs-icon :icon="item.icon" class="fs-16 color-blue mr-5" />
@@ -12,5 +12,11 @@
<script lang="ts" setup>
const props = defineProps<{
options: { value: any; label: string; icon: string }[];
value: any;
}>();
const emit = defineEmits(["update:value"]);
function onChange(value: any) {
emit("update:value", value);
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<icon-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="onChanged"> </icon-select>
<icon-select class="dns-provider-selector" :value="modelValue" :options="options" @update:value="atChange"> </icon-select>
</template>
<script lang="ts">
@@ -37,7 +37,7 @@ export default {
}
onCreate();
function onChanged(value: any) {
function atChange(value: any) {
ctx.emit("update:modelValue", value);
onSelectedChange(value);
}
@@ -52,7 +52,7 @@ export default {
}
return {
options,
onChanged,
atChange,
};
},
};

View File

@@ -28,4 +28,10 @@ export const Dicts = {
{ label: "SSH(已废弃请选择SFTP方式)", value: "ssh", disabled: true },
],
}),
domainFromTypeDict: dict({
data: [
{ value: "manual", label: "手动" },
{ value: "auto", label: "自动" },
],
}),
};

View File

@@ -13,6 +13,13 @@ export default {
title: "Operation",
},
},
pipelinePage: {
addMore: "Add More Pipelines",
aliyunSubscriptionPipeline: "Aliyun Subscription Pipeline",
legoCertPipeline: "Lego Certificate Pipeline",
customPipeline: "Custom Pipeline",
batchAddPipeline: "Add Pipeline Use Template",
},
order: {
confirmTitle: "Order Confirmation",
package: "Package",
@@ -36,6 +43,7 @@ export default {
title: "Framework",
home: "Home",
},
helpDocLink: "Help Docs",
title: "Certificate Automation",
pipeline: "Pipeline",
pipelineEdit: "Edit Pipeline",
@@ -721,6 +729,7 @@ export default {
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.",
cnameDomainPattern: "Domain name cannot contain *",
dnsProvider: "DNS Provider",
dnsProviderAuthorization: "DNS Provider Authorization",
setDefault: "Set Default",
@@ -831,6 +840,8 @@ export default {
disabled: "Disabled",
challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page",
fromType: "From Type",
expirationDate: "Expiration Date",
},
addonSelector: {
select: "Select",

View File

@@ -17,6 +17,13 @@ export default {
title: "操作列",
},
},
pipelinePage: {
addMore: "添加更多流水线",
aliyunSubscriptionPipeline: "阿里云订阅流水线",
legoCertPipeline: "Lego证书流水线",
customPipeline: "自定义流水线",
batchAddPipeline: "模版批量创建流水线",
},
order: {
confirmTitle: "订单确认",
package: "套餐",
@@ -40,7 +47,7 @@ export default {
title: "框架",
home: "首页",
},
helpDocLink: "帮助文档",
title: "证书自动化",
pipeline: "证书自动化流水线",
pipelineEdit: "编辑流水线",
@@ -729,6 +736,7 @@ export default {
cnameDomain: "CNAME域名",
cnameDomainPlaceholder: "cname.handsfree.work",
cnameDomainHelper: "需要一个右边DNS提供商注册的域名也可以将其他域名的dns服务器转移到这几家来。\nCNAME域名一旦确定不可修改建议使用一级子域名",
cnameDomainPattern: "域名不能使用星号",
dnsProvider: "DNS提供商",
dnsProviderAuthorization: "DNS提供商授权",
setDefault: "设置默认",
@@ -810,7 +818,7 @@ export default {
oauthAutoRedirect: "自动跳转第三方登录",
oauthAutoRedirectHelper: "是否自动跳转第三方登录(使用第一个已启用的第三方登录类型)",
oauthOnly: "仅使用第三方登录",
oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启",
oauthOnlyHelper: "是否仅使用第三方登录,关闭密码登录(注意:请务必在测试第三方登录功能正常后再开启,否则会导致无法登录)\n 如果无法登录,请访问 http://你的certd地址/#/login?oauthOnly=false 来临时关闭此模式",
email: {
templates: "邮件模板",
@@ -845,6 +853,8 @@ export default {
disabled: "禁用/启用",
challengeSetting: "校验配置",
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
fromType: "来源类型",
expirationDate: "到期时间",
},
addonSelector: {
select: "选择",

View File

@@ -0,0 +1,38 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
export type FormOptionReq = {
title: string;
columns: any;
onSubmit?: any;
};
export function useFormDialog() {
const { openCrudFormDialog } = useFormWrapper();
async function openFormDialog(req: FormOptionReq) {
function createCrudOptions() {
return {
crudOptions: {
columns: req.columns,
form: {
wrapper: {
title: req.title,
saveRemind: false,
},
async afterSubmit() {},
async doSubmit({ form }: any) {
if (req.onSubmit) {
await req.onSubmit(form);
}
},
},
},
};
}
const { crudOptions } = createCrudOptions();
await openCrudFormDialog({ crudOptions });
}
return {
openFormDialog,
};
}

View File

@@ -72,7 +72,7 @@ export default defineComponent({
}
async function emitValue(value) {
if (pipeline?.value && target?.value && pipeline.value.userId !== target.value.userId) {
if (pipeline && pipeline?.value && target?.value && pipeline.value.userId !== target.value.userId) {
message.error("对不起,您不能修改他人流水线的授权");
return;
}

View File

@@ -57,3 +57,18 @@ export async function DeleteBatch(ids: any[]) {
data: { ids },
});
}
export async function SyncSubmit(body: any) {
return await request({
url: apiPrefix + "/sync/import",
method: "post",
data: body,
});
}
export async function SyncDomainsExpiration() {
return await request({
url: apiPrefix + "/sync/expiration",
method: "post",
});
}

View File

@@ -7,7 +7,8 @@ import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { createAccessApi } from "/@/views/certd/access/api";
import { Modal } from "ant-design-vue";
import { Modal, notification } from "ant-design-vue";
import { useDomainImport } from "./use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -49,6 +50,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
});
const openDomainImportDialog = useDomainImport();
return {
crudOptions: {
settings: {
@@ -88,6 +91,45 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}
},
},
actionbar: {
buttons: {
add: {
icon: "ion:add-circle-outline",
},
import: {
title: "从域名提供商导入域名",
type: "primary",
text: "从域名提供商导入",
needPlus: true,
color: "gold",
icon: "mingcute:vip-1-line",
click: () => {
openDomainImportDialog({
afterSubmit: () => {
setTimeout(() => {
crudExpose.doRefresh();
}, 2000);
},
});
},
},
syncExpirationDate: {
title: "同步域名过期时间",
type: "primary",
icon: "ion:refresh-outline",
text: "同步域名过期时间",
click: async () => {
await api.SyncDomainsExpiration();
notification.success({
message: "同步任务已提交",
});
setTimeout(() => {
crudExpose.doRefresh();
}, 2000);
},
},
},
},
columns: {
id: {
title: "ID",
@@ -119,6 +161,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
sorter: true,
},
},
expirationDate: {
title: t("certd.domain.expirationDate"),
type: "date",
form: {
show: false,
},
},
challengeType: {
title: t("certd.domain.challengeType"),
type: "dict-select",
@@ -285,6 +334,16 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
},
},
},
fromType: {
title: t("certd.domain.fromType"),
type: "dict-select",
dict: Dicts.domainFromTypeDict,
column: {
component: {
color: "auto",
},
},
},
disabled: {
title: t("certd.domain.disabled"),
type: "dict-switch",

View File

@@ -0,0 +1,67 @@
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
import { compute } from "@fast-crud/fast-crud";
export function useDomainImport() {
const { openFormDialog } = useFormDialog();
const columns = {
dnsProviderType: {
title: "域名提供商",
type: "text",
form: {
component: {
name: "dns-provider-selector",
},
on: {
//@ts-ignore
onSelectedChange: ({ form, $event }) => {
form.dnsProviderAccessType = $event.accessType;
},
},
//@ts-ignore
valueChange({ form }) {
form.dnsProviderAccessId = null;
},
},
},
dnsProviderAccessType: {
title: "域名提供商访问类型",
type: "text",
form: {
show: false,
},
},
dnsProviderAccessId: {
title: "域名提供商授权",
type: "text",
form: {
component: {
name: "access-selector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderAccessType || form.dnsProviderType;
}),
},
},
},
};
return function openDomainImportDialog(req: { afterSubmit?: () => void }) {
openFormDialog({
title: "从域名提供商导入域名",
columns: columns,
onSubmit: async (form: any) => {
await api.SyncSubmit({
dnsProviderType: form.dnsProviderType,
dnsProviderAccessId: form.dnsProviderAccessId,
});
message.success("导入任务已提交");
if (req.afterSubmit) {
req.afterSubmit();
}
},
});
};
}

View File

@@ -77,3 +77,11 @@ export async function ResetStatus(id: number) {
},
});
}
export async function Import(form: { domainList: string; cnameProviderId: any }) {
return await request({
url: apiPrefix + "/import",
method: "post",
data: form,
});
}

View File

@@ -7,7 +7,9 @@ import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { message, Modal } from "ant-design-vue";
import CnameTip from "/@/components/plugins/cert/domains-verify-plan-editor/cname-tip.vue";
import { useCnameImport } from "./use";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const crudBinding = crudExpose.crudBinding;
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -27,10 +29,13 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
return res;
};
const openCnameImportDialog = useCnameImport();
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const dictRef = dict({
data: [
{ label: t("certd.pending_cname_setup"), value: "cname", color: "warning" },
@@ -64,6 +69,32 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
editRequest,
delRequest,
},
actionbar: {
buttons: {
import: {
title: "导入CNAME记录",
type: "primary",
text: "批量导入",
click: () => {
openCnameImportDialog({
afterSubmit: () => {
setTimeout(() => {
crudExpose?.doRefresh();
}, 2000);
},
});
},
},
export: {
title: "导出CNAME记录之后可用于批量导入cname解析到域名注册商",
type: "primary",
text: "批量导出",
click: () => {
crudBinding.value.toolbar.buttons.export.click({});
},
},
},
},
tabs: {
name: "status",
show: true,

View File

@@ -0,0 +1,56 @@
import { dict } from "@fast-crud/fast-crud";
import { message } from "ant-design-vue";
import * as api from "./api";
import { useFormDialog } from "/@/use/use-dialog";
export const cnameProviderDict = dict({
url: "/cname/provider/list",
value: "id",
label: "domain",
});
export function useCnameImport() {
const { openFormDialog } = useFormDialog();
const columns = {
domainList: {
title: "域名列表",
type: "text",
form: {
component: {
name: "a-textarea",
rows: 5,
},
col: {
span: 24,
},
required: true,
helper: "每个域名一行,批量导入\n泛域名请去掉*.\n已经存在的会自动跳过",
},
},
cnameProviderId: {
title: "CNAME服务",
type: "dict-select",
dict: cnameProviderDict,
form: {
required: true,
},
},
};
return function openCnameImportDialog(req: { afterSubmit?: () => void }) {
openFormDialog({
title: "导入CNAME记录",
columns: columns,
onSubmit: async (form: any) => {
await api.Import({
domainList: form.domainList,
cnameProviderId: form.cnameProviderId,
});
message.success("导入任务已提交");
if (req.afterSubmit) {
req.afterSubmit();
}
},
});
};
}

View File

@@ -85,46 +85,23 @@ export function useCertPipelineCreator() {
const settingStore = useSettingStore();
const router = useRouter();
function createCrudOptions(certPlugins: any[], getFormData: any, doSubmit: any): CreateCrudOptionsRet {
function createCrudOptions(req: { certPlugin: any; doSubmit: any; title?: string }): CreateCrudOptionsRet {
const inputs: any = {};
const moreParams = [];
for (const plugin of certPlugins) {
for (const inputKey in plugin.input) {
if (inputs[inputKey]) {
//如果两个插件有的字段,直接显示
inputs[inputKey].form.show = true;
continue;
}
const inputDefine = cloneDeep(plugin.input[inputKey]);
if (!inputDefine.required && !inputDefine.maybeNeed) {
moreParams.push(inputKey);
// continue;
}
useReference(inputDefine);
inputs[inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
show: compute(ctx => {
const form = getFormData();
if (!form) {
return false;
}
let inputDefineShow = true;
if (inputDefine.show != null) {
const computeShow = inputDefine.show as any;
if (computeShow === false) {
inputDefineShow = false;
} else if (computeShow && computeShow.computeFn) {
inputDefineShow = computeShow.computeFn({ form });
}
}
return form?.certApplyPlugin === plugin.name && inputDefineShow;
}),
},
};
const doSubmit = req.doSubmit;
for (const inputKey in req.certPlugin.input) {
// inputs[inputKey].form.show = true;
const inputDefine = cloneDeep(req.certPlugin.input[inputKey]);
if (inputDefine.maybeNeed) {
moreParams.push(inputKey);
}
useReference(inputDefine);
inputs[inputKey] = {
title: inputDefine.title,
form: {
...inputDefine,
},
};
}
const pluginStore = usePluginStore();
@@ -146,7 +123,7 @@ export function useCertPipelineCreator() {
wrapClassName: "cert_pipeline_create_form",
width: 1350,
saveRemind: false,
title: t("certd.pipelineForm.createTitle"),
title: req.title || t("certd.pipelineForm.createTitle"),
},
group: {
groups: {
@@ -159,44 +136,44 @@ export function useCertPipelineCreator() {
},
},
columns: {
certApplyPlugin: {
title: t("certd.plugin.selectTitle"),
type: "dict-select",
dict: dict({
data: [
{ value: "CertApply", label: "JS-ACME" },
{ value: "CertApplyLego", label: "Lego-ACME" },
{ value: "CertApplyGetFormAliyun", label: "Aliyun-Order" },
],
}),
form: {
order: 0,
value: "CertApply",
helper: {
render: () => {
return (
<ul>
<li>{t("certd.plugin.jsAcme")}</li>
<li>{t("certd.plugin.legoAcme")}</li>
<li>{t("certd.plugin.aliyunOrder")}</li>
</ul>
);
},
},
valueChange: {
handle: async ({ form, value }) => {
const config = await pluginStore.getPluginConfig({
name: value,
type: "builtIn",
});
if (config.sysSetting?.input) {
merge(form, config.sysSetting.input);
}
},
immediate: true,
},
},
},
// certApplyPlugin: {
// title: t("certd.plugin.selectTitle"),
// type: "dict-select",
// dict: dict({
// data: [
// { value: "CertApply", label: "JS-ACME" },
// { value: "CertApplyLego", label: "Lego-ACME" },
// { value: "CertApplyGetFormAliyun", label: "Aliyun-Order" },
// ],
// }),
// form: {
// order: 0,
// value: "CertApply",
// helper: {
// render: () => {
// return (
// <ul>
// <li>{t("certd.plugin.jsAcme")}</li>
// <li>{t("certd.plugin.legoAcme")}</li>
// <li>{t("certd.plugin.aliyunOrder")}</li>
// </ul>
// );
// },
// },
// valueChange: {
// handle: async ({ form, value }) => {
// const config = await pluginStore.getPluginConfig({
// name: value,
// type: "builtIn",
// });
// if (config.sysSetting?.input) {
// merge(form, config.sysSetting.input);
// }
// },
// immediate: true,
// },
// },
// },
...inputs,
triggerCron: {
title: t("certd.pipelineForm.triggerCronTitle"),
@@ -336,18 +313,10 @@ export function useCertPipelineCreator() {
return certPlugins;
}
async function openAddCertdPipelineDialog(req: { defaultGroupId?: number }) {
async function openAddCertdPipelineDialog(req: { pluginName: string; defaultGroupId?: number; title?: string }) {
//检查是否流水线数量超出限制
await checkPipelineLimit();
const wrapperRef = ref();
function getFormData() {
if (!wrapperRef.value) {
return null;
}
return wrapperRef.value.getFormData();
}
async function doSubmit({ form }: any) {
// const certDetail = readCertDetail(form.cert.crt);
// 添加certd pipeline
@@ -375,7 +344,7 @@ export function useCertPipelineCreator() {
strategy: {
runStrategy: 0, // 正常执行
},
type: form.certApplyPlugin,
type: req.pluginName,
},
],
},
@@ -410,11 +379,19 @@ export function useCertPipelineCreator() {
router.push({ path: "/certd/pipeline/detail", query: { id, editMode: "true" } });
}
const certPlugins = await getCertPlugins();
const { crudOptions } = createCrudOptions(certPlugins, getFormData, doSubmit);
const certPlugin = certPlugins.find(plugin => plugin.name === req.pluginName);
if (!certPlugin) {
message.error("该证书申请插件不存在");
return;
}
const { crudOptions } = createCrudOptions({
certPlugin,
doSubmit,
title: req.title,
});
//@ts-ignore
crudOptions.columns.groupId.form.value = req.defaultGroupId || undefined;
const wrapper = await openCrudFormDialog({ crudOptions });
wrapperRef.value = wrapper;
await openCrudFormDialog({ crudOptions });
}
return {

View File

@@ -1,29 +1,26 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { Modal, notification } from "ant-design-vue";
import dayjs from "dayjs";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, useUi } from "@fast-crud/fast-crud";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { Modal, notification } from "ant-design-vue";
import { useUserStore } from "/@/store/user";
import dayjs from "dayjs";
import * as api from "./api";
import { GetDetail } from "./api";
import { groupDictRef } from "./group/dicts";
import { useSettingStore } from "/@/store/settings";
import { cloneDeep } from "lodash-es";
import { eachStages } from "./utils";
import { setRunnableIds, useCertPipelineCreator } from "/@/views/certd/pipeline/certd-form/use";
import { useUserStore } from "/@/store/user";
import { useCertUpload } from "/@/views/certd/pipeline/cert-upload/use";
import { setRunnableIds } from "/@/views/certd/pipeline/certd-form/use";
import GroupSelector from "/@/views/certd/pipeline/group/group-selector.vue";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
import { useCertViewer } from "/@/views/certd/pipeline/use";
import { useI18n } from "/src/locales";
import { GetDetail, GetObj } from "./api";
import { groupDictRef } from "./group/dicts";
export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
export default function ({ crudExpose, context: { selectedRowKeys, openCertApplyDialog } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const lastResRef = ref();
const { t } = useI18n();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
const { openUploadCreateDialog } = useCertUpload();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
@@ -51,7 +48,10 @@ export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCru
delete form.lastVars;
delete form.createTime;
delete form.id;
let pipeline = JSON.parse(form.content);
let pipeline = form.content;
if (typeof pipeline === "string" && pipeline.startsWith("{")) {
pipeline = JSON.parse(form.content);
}
pipeline.title = form.title;
pipeline = setRunnableIds(pipeline);
form.content = JSON.stringify(pipeline);
@@ -105,7 +105,8 @@ export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCru
actionbar: {
buttons: {
add: {
order: 5,
order: 99,
show: false,
icon: "ion:ios-add-circle-outline",
text: t("certd.customPipeline"),
},
@@ -115,9 +116,7 @@ export default function ({ crudExpose, context: { selectedRowKeys } }: CreateCru
type: "primary",
icon: "ion:ios-add-circle-outline",
click() {
const searchForm = crudExpose.getSearchValidatedFormData();
const defaultGroupId = searchForm.groupId;
openAddCertdPipelineDialog({ defaultGroupId });
openCertApplyDialog({ key: "CertApply" });
},
},
uploadCert: {

View File

@@ -9,6 +9,28 @@
</template>
</a-alert> -->
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #actionbar-right>
<a-dropdown class="ml-1">
<a-button type="primary" class="ant-dropdown-link" @click.prevent>
{{ t("certd.pipelinePage.addMore") }}
<DownOutlined />
</a-button>
<template #overlay>
<a-menu @click="onActionbarMoreItemClick">
<!-- <a-menu-item key="CertApplyUpload" class="flex items-center">
<fs-icon icon="ion:business-outline" />
商用证书托管流水线
</a-menu-item> -->
<a-menu-item v-for="item in addMorePipelineBtns" :key="item.key" :title="item.title">
<div class="flex items-center">
<fs-icon :icon="item.icon" />
<span class="ml-2">{{ item.title }}</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<div v-if="selectedRowKeys.length > 0" class="batch-actions">
<div class="batch-actions-inner">
<span>{{ t("certd.selectedCount", { count: selectedRowKeys.length }) }}</span>
@@ -19,7 +41,6 @@
<change-trigger :selected-row-keys="selectedRowKeys" @change="batchFinished"></change-trigger>
</div>
</div>
<template #actionbar-right> </template>
<template #form-bottom>
<div>{{ t("certd.applyCertificate") }}</div>
</template>
@@ -28,7 +49,7 @@
</template>
<script lang="ts" setup>
import { onActivated, onMounted, ref } from "vue";
import { computed, onActivated, onMounted, ref } from "vue";
import { dict, useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import ChangeGroup from "./components/change-group.vue";
@@ -42,6 +63,8 @@ const { t } = useI18n();
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";
import { useSettingStore } from "/@/store/settings";
import { groupDictRef } from "./group/dicts";
import { useCertPipelineCreator } from "./certd-form/use";
import { useRouter } from "vue-router";
defineOptions({
name: "PipelineManager",
@@ -51,6 +74,36 @@ const selectedRowKeys = ref([]);
const context: any = {
selectedRowKeys,
};
const router = useRouter();
const { openAddCertdPipelineDialog } = useCertPipelineCreator();
function onActionbarMoreItemClick(req: { key: string; item: any }) {
openCertApplyDialog({ key: req.key, title: req.item?.title });
}
const addMorePipelineBtns = computed(() => {
return [
{ key: "CertApplyGetFormAliyun", title: t("certd.pipelinePage.aliyunSubscriptionPipeline"), icon: "svg:icon-aliyun" },
{ key: "CertApplyLego", title: t("certd.pipelinePage.legoCertPipeline"), icon: "cbi:lego" },
{ key: "AddPipeline", title: t("certd.pipelinePage.customPipeline"), icon: "ion:add-circle-outline" },
{ key: "BatchAddPipeline", title: t("certd.pipelinePage.batchAddPipeline"), icon: "ion:duplicate" },
];
});
function openCertApplyDialog(req: { key: string; title: string }) {
if (req.key === "AddPipeline") {
crudExpose.openAdd({});
return;
}
if (req.key === "BatchAddPipeline") {
router.push({ path: "/certd/pipeline/template" });
return;
}
const searchForm = crudExpose.getSearchValidatedFormData();
const defaultGroupId = searchForm.groupId;
openAddCertdPipelineDialog({ pluginName: req.key, defaultGroupId, title: req.title });
}
context.openCertApplyDialog = openCertApplyDialog;
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context });
// 页面打开后获取列表数据

View File

@@ -88,7 +88,10 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
placeholder: t("certd.cnameDomainPlaceholder"),
},
helper: t("certd.cnameDomainHelper"),
rules: [{ required: true, message: t("certd.requiredField") }],
rules: [
{ required: true, message: t("certd.requiredField") },
{ pattern: /^[^*]+$/, message: t("certd.cnameDomainPattern") },
],
},
column: {
width: 200,

View File

@@ -228,7 +228,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rules: [
{ required: true },
{
type: "regexp",
type: "pattern",
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
message: t("certd.pluginNameRuleMsg"),
},
@@ -257,7 +257,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
rules: [
{ required: true },
{
type: "regexp",
type: "pattern",
pattern: /^[a-zA-Z][a-zA-Z0-9]+$/,
message: t("certd.authorRuleMsg"),
},

View File

@@ -34,7 +34,7 @@
<a-select-option value="ipv4first">{{ t("certd.ipv4Priority") }}</a-select-option>
<a-select-option value="ipv6first">{{ t("certd.ipv6Priority") }}</a-select-option>
</a-select>
<div class="helper">{{ t("certd.dualStackNetworkHelper") }}</div>
<div class="helper">{{ t("certd.dualStackNetworkHelper") }}, <a href="https://certd.docmirror.cn/guide/use/setting/ipv6.html" target="_blank">{{ t("certd.helpDocLink") }}</a></div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.showRunStrategy')" :name="['public', 'showRunStrategy']">

View File

@@ -21,7 +21,10 @@
<a-switch v-model:checked="formState.public.certDomainAddToMonitorEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}</div>
<div class="helper">
{{ t("certd.sys.setting.certDomainAddToMonitorEnabledHelper") }}
<a href="https://certd.docmirror.cn/guide/use/setting/user-valid.html" target="_blank">{{ t("certd.helpDocLink") }}</a>
</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.fixedCertExpireDays')" :name="['public', 'fixedCertExpireDays']">

View File

@@ -12,7 +12,10 @@
<a-switch v-model:checked="formState.public.userValidTimeEnabled" :disabled="!settingsStore.isPlus" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">{{ t("certd.userValidityPeriodHelper") }}</div>
<div class="helper">
{{ t("certd.userValidityPeriodHelper") }}
<a href="https://certd.docmirror.cn/guide/use/setting/user-valid.html" target="_blank">{{ t("certd.helpDocLink") }}</a>
</div>
</a-form-item>
<template v-if="formState.public.registerEnabled">
<a-form-item :label="t('certd.enableUsernameRegistration')" :name="['public', 'usernameRegisterEnabled']">

View File

@@ -1,2 +1,2 @@
LEGO_VERSION=4.30.1
certd_plugin_loadmode=metadata
certd_plugin_loadmode=dev

View File

@@ -0,0 +1,3 @@
ALTER TABLE cd_domain ADD COLUMN from_type varchar(20);
ALTER TABLE cd_domain ADD COLUMN registration_date integer;
ALTER TABLE cd_domain ADD COLUMN expiration_date integer;

View File

@@ -1,6 +1,7 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import {DomainService} from "../../../modules/cert/service/domain-service.js";
import { checkPlus } from '@certd/plus-core';
/**
* 授权
@@ -78,4 +79,24 @@ export class DomainController extends CrudController<DomainService> {
return this.ok();
}
@Post('/sync/import', { summary: Constants.per.authOnly })
async syncImport(@Body(ALL) body: any) {
const { dnsProviderType, dnsProviderAccessId } = body;
const req = {
dnsProviderType, dnsProviderAccessId, userId: this.getUserId(),
}
checkPlus()
await this.service.doSyncFromProvider(req);
return this.ok();
}
@Post('/sync/expiration', { summary: Constants.per.authOnly })
async syncExpiration(@Body(ALL) body: any) {
await this.service.doSyncDomainsExpirationDate({
userId: this.getUserId(),
})
return this.ok();
}
}

View File

@@ -99,4 +99,15 @@ export class CnameRecordController extends CrudController<CnameRecordService> {
const res = await this.service.resetStatus(body.id);
return this.ok(res);
}
@Post('/import', { summary: Constants.per.authOnly })
async import(@Body(ALL) body: { domainList: string; cnameProviderId: any }) {
const userId = this.getUserId();
const res = await this.service.doImport({
userId,
domainList: body.domainList,
cnameProviderId: body.cnameProviderId,
});
return this.ok(res);
}
}

View File

@@ -6,11 +6,12 @@ import {SiteInfoService} from '../monitor/index.js';
import {Cron} from '../cron/cron.js';
import {UserSettingsService} from "../mine/service/user-settings-service.js";
import {UserSiteMonitorSetting} from "../mine/service/models.js";
import {getPlusInfo} from "@certd/plus-core";
import {getPlusInfo, isPlus} from "@certd/plus-core";
import dayjs from "dayjs";
import {NotificationService} from "../pipeline/service/notification-service.js";
import {UserService} from "../sys/authority/service/user-service.js";
import {Between} from "typeorm";
import { DomainService } from '../cert/service/domain-service.js';
@Autoload()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -44,6 +45,9 @@ export class AutoCRegisterCron {
@Inject()
userService: UserService;
@Inject()
domainService: DomainService;
@Init()
async init() {
@@ -60,7 +64,9 @@ export class AutoCRegisterCron {
await this.registerPlusExpireCheckCron();
await this.registerUserExpireCheckCron()
await this.registerUserExpireCheckCron();
await this.registerDomainExpireCheckCron();
}
async registerSiteMonitorCron() {
@@ -199,4 +205,23 @@ export class AutoCRegisterCron {
}
})
}
registerDomainExpireCheckCron(){
if (!isPlus()){
return
}
// 添加域名即将到期检查任务
const randomWeek = Math.floor(Math.random() * 7) + 1
const randomHour = Math.floor(Math.random() * 24)
const randomMinute = Math.floor(Math.random() * 60)
logger.info(`注册域名注册过期时间检查任务,每周${randomWeek} ${randomHour}:${randomMinute}检查一次`)
this.cron.register({
name: 'domain-expire-check',
cron: `0 ${randomMinute} ${randomHour} ? * ${randomWeek}`, // 每周随机一天检查一次
job: async () => {
await this.domainService.doSyncDomainsExpirationDate({})
}
})
}
}

View File

@@ -37,7 +37,7 @@ export class AutoZPrint {
logger.info(`当前版本:${version}`);
const plusInfo = getPlusInfo();
if (isPlus()) {
logger.info(`授权信息:${plusInfo.vipType},${dayjs(plusInfo.expireTime).format('YYYY-MM-DD')}`);
logger.info(`授权信息:${plusInfo.vipType},${plusInfo.expireTime === -1 ? '永久' : dayjs(plusInfo.expireTime).format('YYYY-MM-DD')}`);
}
logger.info('Certd已启动');
logger.info('=========================================');

View File

@@ -25,6 +25,15 @@ export class DomainEntity {
@Column({ comment: '是否禁用', name: 'disabled' })
disabled: boolean;
@Column({ comment: '注册时间', name: 'registration_date' })
registrationDate: number;
@Column({ comment: '过期时间', name: 'expiration_date' })
expirationDate: number;
@Column({ comment: '来源', name: 'from_type', length: 50 })
fromType: string;
@Column({ comment: 'http上传类型', name: 'http_uploader_type', length: 50 })
httpUploaderType: string;

View File

@@ -1,21 +1,33 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { http, logger, utils } from '@certd/basic';
import { AccessService, BaseService } from '@certd/lib-server';
import { doPageTurn, Pager, PageRes } from '@certd/pipeline';
import { DomainVerifiers } from "@certd/plugin-cert";
import { createDnsProvider, DomainParser, parseDomainByPsl } from "@certd/plugin-lib";
import { Inject, Provide, Scope, ScopeEnum } from '@midwayjs/core';
import { InjectEntityModel } from '@midwayjs/typeorm';
import dayjs from 'dayjs';
import { In, Not, Repository } from 'typeorm';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { TaskServiceBuilder } from '../../pipeline/service/getter/task-service-getter.js';
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { DomainEntity } from '../entity/domain.js';
import { BackTask, taskExecutor } from './task-executor.js';
export interface SyncFromProviderReq {
userId: number;
dnsProviderType: string;
dnsProviderAccessId: string;
}
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class DomainService extends BaseService<DomainEntity> {
@InjectEntityModel(DomainEntity)
repository: Repository<DomainEntity>;
@@ -28,13 +40,16 @@ export class DomainService extends BaseService<DomainEntity> {
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
taskServiceBuilder: TaskServiceBuilder;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(param) {
if (param.userId == null ){
if (param.userId == null) {
throw new Error('userId 不能为空');
}
if (!param.domain) {
@@ -49,6 +64,9 @@ export class DomainService extends BaseService<DomainEntity> {
if (old) {
throw new Error(`域名(${param.domain})不能重复`);
}
if (!param.fromType) {
param.fromType = 'manual'
}
return await super.add(param);
}
@@ -83,9 +101,9 @@ export class DomainService extends BaseService<DomainEntity> {
* @param userId
* @param domains //去除* 且去重之后的域名列表
*/
async getDomainVerifiers(userId: number, domains: string[]):Promise<DomainVerifiers> {
async getDomainVerifiers(userId: number, domains: string[]): Promise<DomainVerifiers> {
const mainDomainMap:Record<string, string> = {}
const mainDomainMap: Record<string, string> = {}
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
@@ -97,7 +115,7 @@ export class DomainService extends BaseService<DomainEntity> {
}
//匹配DNS记录
let allDomains = [...domains,...mainDomains]
let allDomains = [...domains, ...mainDomains]
//去重
allDomains = [...new Set(allDomains)]
@@ -106,16 +124,16 @@ export class DomainService extends BaseService<DomainEntity> {
where: {
domain: In(allDomains),
userId,
disabled:false,
disabled: false,
}
})
const dnsMap = domainRecords.filter(item=>item.challengeType === 'dns').reduce((pre, item) => {
const dnsMap = domainRecords.filter(item => item.challengeType === 'dns').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
const httpMap = domainRecords.filter(item=>item.challengeType === 'http').reduce((pre, item) => {
const httpMap = domainRecords.filter(item => item.challengeType === 'http').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
@@ -136,7 +154,7 @@ export class DomainService extends BaseService<DomainEntity> {
}, {})
//构建域名验证计划
const domainVerifiers:DomainVerifiers = {}
const domainVerifiers: DomainVerifiers = {}
for (const domain of domains) {
const mainDomain = mainDomainMap[domain]
@@ -154,7 +172,7 @@ export class DomainService extends BaseService<DomainEntity> {
}
continue
}
const cnameRecord:CnameRecordEntity = cnameMap[domain]
const cnameRecord: CnameRecordEntity = cnameMap[domain]
if (cnameRecord) {
domainVerifiers[domain] = {
domain,
@@ -180,11 +198,200 @@ export class DomainService extends BaseService<DomainEntity> {
httpUploadRootDir: httpRecord.httpUploadRootDir
}
}
continue
continue
}
domainVerifiers[domain] = null
}
return domainVerifiers;
}
async doSyncFromProvider(req: SyncFromProviderReq) {
taskExecutor.start('syncFromProviderTask', new BackTask({
key: `user_${req.userId}`,
title: `同步用户${req.userId}从域名提供商导入域名`,
run: async (task: BackTask) => {
await this._syncFromProvider(req, task)
},
}))
}
private async _syncFromProvider(req: SyncFromProviderReq, task: BackTask) {
const { userId, dnsProviderType, dnsProviderAccessId } = req;
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const serviceGetter = this.taskServiceBuilder.create({ userId });
const access = await this.accessService.getById(dnsProviderAccessId, userId);
const context = { access, logger, http, utils, domainParser, serviceGetter };
// 翻页查询dns的记录
const dnsProvider = await createDnsProvider({ dnsProviderType, context })
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const challengeType = "dns"
const importDomain = async (domainRecord: any) => {
task.incrementCurrent()
const domain = domainRecord.domain
const old = await this.findOne({
where: {
domain,
userId,
}
})
if (old) {
if (old.fromType !== 'auto') {
//如果是手动的,跳过更新校验配置
return
}
const updateObj: any = {
id: old.id,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
}
//更新
await super.update(updateObj)
} else {
//添加
await this.add({
userId,
domain,
dnsProviderType,
dnsProviderAccess: dnsProviderAccessId,
challengeType,
disabled: false,
fromType: 'auto',
})
}
}
const batchHandle = async (pageRes: PageRes<any>) => {
task.setTotal(pageRes.total || 0)
}
const start = async () => {
await doPageTurn({ pager, getPage: dnsProvider.getDomainListPage, itemHandle: importDomain, batchHandle })
}
start()
}
async doSyncDomainsExpirationDate(req: { userId?: number }) {
const userId = req.userId
taskExecutor.start('syncDomainsExpirationDateTask', new BackTask({
key: `user_${userId}`,
title: `同步用户(${userId ?? '全部'})注册域名过期时间`,
run: async (task: BackTask) => {
await this._syncDomainsExpirationDate({ userId, task })
}
}))
}
private async _syncDomainsExpirationDate(req: { userId?: number, task: BackTask }) {
//同步所有域名的过期时间
const pager = new Pager({
pageNo: 1,
pageSize: 100,
})
const dnsJson = await http.request({
url: "https://data.iana.org/rdap/dns.json",
method: "GET",
})
const rdapMap: Record<string, string> = {}
for (const item of dnsJson.services) {
// [["store","work"], ["https://rdap.centralnic.com/store/"]],
const suffixes = item[0]
const urls = item[1]
for (const suffix of suffixes) {
rdapMap[suffix] = urls[0]
}
}
const getDomainExpirationDate = async (domain: string) => {
const parsed = parseDomainByPsl(domain)
const mainDomain = parsed.domain || ''
if (mainDomain !== domain) {
logger.warn(`${domain}为子域名,跳过同步`)
return
}
const suffix = parsed.tld || ''
const rdapUrl = rdapMap[suffix]
if (!rdapUrl) {
throw new Error(`未找到${suffix}的rdap地址`)
}
// https://rdap.nic.work/domain/handsfree.work
const rdap = await http.request({
url: `${rdapUrl}domain/${domain}`,
method: "GET",
})
let res: any = {}
const events = rdap.events || []
for (const item of events) {
if (item.eventAction === 'expiration') {
res.expirationDate = dayjs(item.eventDate).valueOf()
} else if (item.eventAction === 'registration') {
res.registrationDate = dayjs(item.eventDate).valueOf()
}
}
return res
}
const query: any = {
challengeType: "dns",
}
if (req.userId!=null) {
query.userId = req.userId
}
const getDomainPage = async (pager: Pager) => {
const pageRes = await this.page({
query: query,
// buildQuery(bq) {
// bq.andWhere(" (expiration_date is null or expiration_date < :now) ", { now: dayjs().add(1, 'month').valueOf() })
// },
page: {
offset: pager.getOffset(),
limit: pager.pageSize,
}
})
req.task.total = pageRes.total
return {
list: pageRes.records,
total: pageRes.total,
}
}
const itemHandle = async (item: any) => {
req.task.incrementCurrent()
try {
const res = await getDomainExpirationDate(item.domain)
if (!res) {
return
}
const { expirationDate, registrationDate } = res
if (!expirationDate) {
logger.error(`获取域名${item.domain}过期时间失败`)
return
}
logger.info(`更新域名${item.domain}过期时间:${dayjs(expirationDate).format('YYYY-MM-DD')}`)
const updateObj: any = {
id: item.id,
expirationDate: expirationDate,
registrationDate: registrationDate,
}
//更新
await super.update(updateObj)
} catch (error) {
logger.error(`更新域名${item.domain}过期时间失败:${error}`)
} finally {
await utils.sleep(1000)
}
}
await doPageTurn({ pager, getPage: getDomainPage, itemHandle: itemHandle })
}
}

View File

@@ -0,0 +1,106 @@
import { logger } from "@certd/basic"
export class BackTaskExecutor {
tasks: Record<string, Record<string, BackTask>> = {}
start(type: string, task: BackTask) {
if (!this.tasks[type]) {
this.tasks[type] = {}
}
const oldTask = this.tasks[type][task.key]
if (oldTask && oldTask.status === "running") {
throw new Error(`任务 ${type}${task.key} 正在运行中`)
}
this.tasks[type][task.key] = task
this.run(type, task);
}
get(type: string, key: string) {
if (!this.tasks[type]) {
this.tasks[type] = {}
}
return this.tasks[type][key]
}
removeIsEnd(type: string, key: string) {
const task = this.tasks[type]?.[key]
if (task && task.status !== "running") {
this.clear(type, key);
}
}
clear(type: string, key: string) {
const task = this.tasks[type]?.[key]
if (task) {
task.clearTimeout();
delete this.tasks[type][key]
}
}
private async run(type: string, task: any) {
if (task.status === "running") {
throw new Error(`任务 ${type}${task.key} 正在运行中`)
}
task.startTime = Date.now();
task.clearTimeout();
try {
task.status = "running";
return await task.run(task);
} catch (e) {
logger.error(`任务 ${task.title}[${type}-${task.key}] 执行失败`, e.message);
task.status = "failed";
task.error = e.message;
} finally {
task.endTime = Date.now();
task.status = "done";
task.timeoutId = setTimeout(() => {
this.clear(type, task.key);
}, 24 * 60 * 60 * 1000);
delete task.run;
}
}
}
export class BackTask {
key: string;
title: string;
total: number = 0;
current: number = 0;
startTime: number;
endTime: number;
status: string = "pending";
error?: string;
timeoutId?: NodeJS.Timeout;
run: (task: BackTask) => Promise<void>;
constructor(opts:{
key: string, title: string, run: (task: BackTask) => Promise<void>
}) {
const {key, title, run} = opts
this.key = key;
this.title = title;
Object.defineProperty(this, 'run', {
value: run,
writable: true,
enumerable: false, // 设置为false使其不可遍历
configurable: true
});
}
clearTimeout() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
this.timeoutId = null;
}
}
setTotal(total: number) {
this.total = total;
}
incrementCurrent() {
this.current++
}
}
export const taskExecutor = new BackTaskExecutor();

View File

@@ -22,6 +22,7 @@ import punycode from "punycode.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
import { TaskServiceBuilder } from "../../pipeline/service/getter/task-service-getter.js";
import { BackTask, taskExecutor } from "../../cert/service/task-executor.js";
type CnameCheckCacheValue = {
validating: boolean;
@@ -487,4 +488,49 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
await this.getRepository().update(id, { status: "cname", mainDomain: "" });
}
async doImport(req:{ userId: number; domainList: string; cnameProviderId: any }) {
const {userId,cnameProviderId,domainList} = req;
const domains = domainList.split("\n").map(item => item.trim()).filter(item => item.length > 0);
if (domains.length === 0) {
throw new ValidateException("域名列表不能为空");
}
if (!req.cnameProviderId) {
throw new ValidateException("CNAME服务提供商不能为空");
}
taskExecutor.start("cnameImport",new BackTask({
key: "user_"+userId,
title: "导入CNAME记录",
run: async (task) => {
await this._import({ userId, domains, cnameProviderId },task);
}
}));
}
async _import(req :{ userId: number; domains: string[]; cnameProviderId: any },task:BackTask) {
const userId = req.userId;
for (const domain of req.domains) {
const old = await this.getRepository().findOne({
where: {
userId: req.userId,
domain,
},
});
if (old) {
logger.warn(`域名${domain}已存在,跳过`);
}
//开始导入
try{
await this.add({
userId,
domain: domain,
cnameProviderId: req.cnameProviderId,
});
}catch(e){
logger.error(`导入域名${domain}失败:${e.message}`);
}
}
}
}

View File

@@ -1,6 +1,7 @@
import {CreateRecordOptions, DnsProviderContext, IDnsProvider, RemoveRecordOptions} from '@certd/plugin-cert';
import {CreateRecordOptions, DnsProviderContext, DomainRecord, IDnsProvider, RemoveRecordOptions} from '@certd/plugin-cert';
import {PlusService} from '@certd/lib-server';
import punycode from 'punycode.js'
import { Pager, PageRes } from '@certd/pipeline';
export type CommonCnameProvider = {
id: number;
domain: string;
@@ -23,6 +24,9 @@ export class CommonDnsProvider implements IDnsProvider {
this.config = opts.config;
this.plusService = opts.plusService;
}
getDomainListPage(pager: Pager): Promise<PageRes<DomainRecord>> {
throw new Error('公共CNAME服务不支持获取域名列表');
}
/**
* 中文转英文

View File

@@ -286,6 +286,8 @@ export class PluginService extends BaseService<PluginEntity> {
dnsProviderRegistry.unRegister(name)
} else if (item.pluginType === "notification") {
notificationRegistry.unRegister(name)
}else if (item.pluginType === "addon") {
addonRegistry.unRegister(name)
} else {
logger.warn(`不支持的插件类型:${item.pluginType}`)
}
@@ -303,7 +305,7 @@ export class PluginService extends BaseService<PluginEntity> {
throw new Error(`插件${param.author}/${param.name}已存在`);
}
await this.unRegisterById(param.id);
const res = await super.update(param);
await this.registerById(param.id);
@@ -411,7 +413,7 @@ export class PluginService extends BaseService<PluginEntity> {
delete item.metadata;
delete item.content;
delete item.extra;
if (item.author) {
if (item.author && !item.name.startsWith(`${item.author}/`)) {
item.name = item.author + "/" + item.name;
}
let name = item.name
@@ -525,10 +527,11 @@ export class PluginService extends BaseService<PluginEntity> {
async deleteByIds(ids: any[]) {
await super.delete(ids);
for (const id of ids) {
await this.unRegisterById(id)
await this.delete(id);
}
}

View File

@@ -1,6 +1,7 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from '@certd/plugin-cert';
import { AliyunAccess } from '../../plugin-lib/aliyun/access/aliyun-access.js';
import { AliyunClient } from '../../plugin-lib/aliyun/index.js';
import { Pager, PageRes } from '@certd/pipeline';
@IsDnsProvider({
@@ -153,6 +154,33 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
throw e
}
}
async getDomainListPage(pager: Pager) :Promise<PageRes<DomainRecord>> {
const params = {
RegionId: 'cn-hangzhou',
PageSize: pager.pageSize,
PageNumber: pager.pageNo,
};
const requestOption = {
method: 'POST',
};
const ret = await this.client.request(
'DescribeDomains',
params,
requestOption
);
const list = ret.Domains?.Domain?.map(item => ({
id: item.DomainId,
domain: item.DomainName,
})) || []
return {
list,
total: ret.TotalCount,
}
}
}
new AliyunDnsProvider();

View File

@@ -130,7 +130,7 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
try{
await this.clearSiteCert(client,siteId);
}catch (e) {
this.logger.error("清理站点[${siteId}]证书失败",e)
this.logger.error(`清理站点[${siteId}]证书失败`,e)
}
try {
@@ -160,7 +160,7 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
try{
await this.clearSiteCert(client,siteId);
}catch (e) {
this.logger.error("清理站点[${siteId}]证书失败",e)
this.logger.error(`清理站点[${siteId}]证书失败`,e)
}
}
}

View File

@@ -123,25 +123,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
})
challengeType!: string;
@TaskInput({
title: "证书颁发机构",
value: "letsencrypt",
component: {
name: "icon-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt免费新手推荐支持IP证书", icon: "simple-icons:letsencrypt" },
{ 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仅主域名和www免费", icon: "la:expeditedssl" },
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境仅供测试", icon: "simple-icons:letsencrypt" },
],
},
helper: "Let's Encrypt申请最简单\nGoogle大厂光环兼容性好仅首次需要翻墙获取EAB授权\nZeroSSL需要EAB授权无需翻墙\nSSL.com仅主域名和www免费,必须设置CAA记录",
required: true,
})
sslProvider!: SSLProvider;
@TaskInput({
title: "DNS解析服务商",
@@ -227,6 +209,27 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
})
domainsVerifyPlan!: DomainsVerifyPlanInput;
@TaskInput({
title: "证书颁发机构",
value: "letsencrypt",
component: {
name: "icon-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt免费新手推荐支持IP证书", icon: "simple-icons:letsencrypt" },
{ 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仅主域名和www免费", icon: "la:expeditedssl" },
{ value: "letsencrypt_staging", label: "Let's Encrypt测试环境仅供测试", icon: "simple-icons:letsencrypt" },
],
},
helper: "Let's Encrypt申请最简单\nGoogle大厂光环兼容性好仅首次需要翻墙获取EAB授权\nZeroSSL需要EAB授权无需翻墙\nSSL.com仅主域名和www免费,必须设置CAA记录",
required: true,
})
sslProvider!: SSLProvider;
@TaskInput({
title: "Google公共EAB授权",
isSys: true,
@@ -319,6 +322,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
],
},
helper: "如无特殊需求,默认即可\n选择RSA 2048 pkcs1可以获得旧版RSA证书",
maybeNeed: false,
required: true,
})
privateKeyType!: PrivateKeyType;
@@ -337,6 +341,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
},
helper: "如无特殊需求,默认即可",
required: false,
maybeNeed: true,
mergeScript: `
return {
show: ctx.compute(({form})=>{
@@ -356,6 +361,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
},
helper: preferredChainConfigs.letsencrypt.helper,
required: false,
maybeNeed: true,
mergeScript: preferredChainMergeScript,
})
preferredChain!: string;
@@ -367,6 +373,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "a-switch",
vModel: "checked",
},
maybeNeed: true,
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问请尝试开启此选项\n默认情况会进行测试如果无法访问将会自动使用代理",
})
useProxy = false;
@@ -376,6 +383,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
component: {
placeholder: "google.yourproxy.com",
},
maybeNeed: true,
helper: "填写你的自定义反代地址不要带http://\nletsencrypt反代目标acme-v02.api.letsencrypt.org\ngoogle反代目标dv.acme-v02.api.pki.goog",
})
reverseProxy = "";
@@ -387,6 +395,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "a-switch",
vModel: "checked",
},
maybeNeed: true,
helper: "跳过本地校验可以加快申请速度,同时也会增加失败概率。",
})
skipLocalVerify = false;
@@ -398,6 +407,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "a-input-number",
vModel: "value",
},
maybeNeed: true,
helper: "检查域名验证解析记录重试次数,如果你的域名服务商解析生效速度慢,可以适当增加此值",
})
maxCheckRetryCount = 20;
@@ -409,6 +419,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
name: "a-input-number",
vModel: "value",
},
maybeNeed: true,
helper: "等待解析生效时长如果使用CNAME方式校验本地验证失败可以尝试延长此时间比如5-10分钟",
})
waitDnsDiffuseTime = 30;

View File

@@ -39,7 +39,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
},
required: false,
order: 100,
helper: "转换成PFX、jks格式证书是否需要加密\njks必须设置密码不传则默认123456\npfx不传则为空密码",
helper: "转换成PFX、jks格式证书是否需要加密\n不传则pfx格式默认空密码jks格式默认123456",
})
pfxPassword!: string;
@@ -57,6 +57,7 @@ export abstract class CertApplyBaseConvertPlugin extends AbstractTaskPlugin {
},
required: false,
order: 100,
maybeNeed: true,
helper: "兼容Windows Server各个版本",
})
pfxArgs = "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES";

View File

@@ -39,6 +39,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
vModel: "checked",
},
order: 100,
maybeNeed: true,
helper: "证书申请成功后是否发送通知,优先使用默认通知渠道",
})
successNotify = false;

View File

@@ -71,7 +71,6 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin {
name: "access-selector",
type: "eab",
},
maybeNeed: true,
helper: "如果需要提供EAB授权",
})
legoEabAccessId!: number;

82
pnpm-lock.yaml generated
View File

@@ -49,7 +49,7 @@ importers:
packages/core/acme-client:
dependencies:
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../basic
'@peculiar/x509':
specifier: ^1.11.0
@@ -213,10 +213,10 @@ importers:
packages/core/pipeline:
dependencies:
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../basic
'@certd/plus-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../pro/plus-core
dayjs:
specifier: ^1.11.7
@@ -412,7 +412,7 @@ importers:
packages/libs/lib-k8s:
dependencies:
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@kubernetes/client-node':
specifier: 0.21.0
@@ -452,19 +452,19 @@ importers:
packages/libs/lib-server:
dependencies:
'@certd/acme-client':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@certd/plugin-lib':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../plugins/plugin-lib
'@certd/plus-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../pro/plus-core
'@midwayjs/cache':
specifier: 3.14.0
@@ -610,16 +610,16 @@ importers:
packages/plugins/plugin-cert:
dependencies:
'@certd/acme-client':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@certd/plugin-lib':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../plugin-lib
psl:
specifier: ^1.9.0
@@ -683,16 +683,16 @@ importers:
specifier: ^3.964.0
version: 3.964.0(aws-crt@1.26.2)
'@certd/acme-client':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@certd/plus-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../pro/plus-core
'@kubernetes/client-node':
specifier: 0.21.0
@@ -783,16 +783,16 @@ importers:
packages/pro/commercial-core:
dependencies:
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@certd/lib-server':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/lib-server
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@certd/plus-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../plus-core
'@midwayjs/core':
specifier: 3.20.11
@@ -865,16 +865,16 @@ importers:
packages/pro/plugin-plus:
dependencies:
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@certd/plugin-lib':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../plugins/plugin-lib
'@certd/plus-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../plus-core
crypto-js:
specifier: ^4.2.0
@@ -950,7 +950,7 @@ importers:
packages/pro/plus-core:
dependencies:
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
dayjs:
specifier: ^1.11.7
@@ -1246,10 +1246,10 @@ importers:
version: 0.1.3(zod@3.24.4)
devDependencies:
'@certd/lib-iframe':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/lib-iframe
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@rollup/plugin-commonjs':
specifier: ^25.0.7
@@ -1438,46 +1438,46 @@ importers:
specifier: ^3.964.0
version: 3.964.0(aws-crt@1.26.2)
'@certd/acme-client':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/acme-client
'@certd/basic':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/basic
'@certd/commercial-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../pro/commercial-core
'@certd/cv4pve-api-javascript':
specifier: ^8.4.2
version: 8.4.2
'@certd/jdcloud':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/lib-jdcloud
'@certd/lib-huawei':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/lib-huawei
'@certd/lib-k8s':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/lib-k8s
'@certd/lib-server':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/lib-server
'@certd/midway-flyway-js':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../libs/midway-flyway-js
'@certd/pipeline':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../core/pipeline
'@certd/plugin-cert':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../plugins/plugin-cert
'@certd/plugin-lib':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../plugins/plugin-lib
'@certd/plugin-plus':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../pro/plugin-plus
'@certd/plus-core':
specifier: ^1.38.0
specifier: ^1.38.1
version: link:../../pro/plus-core
'@google-cloud/publicca':
specifier: ^1.3.0

View File

@@ -17,7 +17,7 @@ export function getVersionContent() {
const contentStart = versionLineIndex + 1
lines = lines.slice(contentStart)
const contentEnd = lines.findIndex(line => {
return line.startsWith('## ')
return line.startsWith('## [') || line.startsWith('# [')
})
const content = lines.slice(0, contentEnd).join('\n')
console.log("-------title------/n")

View File

@@ -66,7 +66,7 @@ async function createRelease(versionTitle, content) {
tag_name: `v${versionTitle}`,
name: `v${versionTitle}`,
body: content,
target_commitish: 'v2'
target_commitish: 'v2-dev'
},
}
)

View File

@@ -20,7 +20,7 @@ async function createRelease(versionTitle, content) {
tag_name: `v${versionTitle}`,
name: `v${versionTitle}`,
body: content,
target_commitish: 'v2'
target_commitish: 'v2-dev'
},
})
console.log("createRelease success")

44
scripts/publish-github.js Normal file
View File

@@ -0,0 +1,44 @@
import axios from 'axios'
import { getVersionContent } from './get-new-version.js'
const GithubAccessToken = process.env.GITHUB_TOKEN
if (!GithubAccessToken) {
console.log("GithubAccessToken is empty")
throw new Error("GithubAccessToken is empty")
}
// 创建release
async function createRelease(versionTitle, content) {
const response = await axios.request({
method: 'POST',
url: `https://api.github.com/repos/certd/certd/releases`,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${GithubAccessToken}`,
},
data: {
tag_name: `v${versionTitle}`,
name: `v${versionTitle}`,
body: content,
target_commitish: 'v2-dev'
},
})
console.log("createRelease success")
return response.data
}
async function publishToGithub() {
try{
const { versionTitle, content } = getVersionContent()
const release = await createRelease(versionTitle, content)
console.log("publishToGithub success")
} catch (error) {
if (error?.response?.data){
console.log("publishToGithub error:",error.response.data)
}else{
console.log("publishToGithub error:",error)
}
}
}
publishToGithub()

View File

@@ -1 +1 @@
2
01:00

View File

@@ -1 +1 @@
3
01:32