Compare commits
24 Commits
v1.38.1
...
v2-domain-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83df29d832 | ||
|
|
607afe864a | ||
|
|
a97cee84f3 | ||
|
|
ad64384891 | ||
|
|
f75c73d739 | ||
|
|
418bcddc95 | ||
|
|
61192b998a | ||
|
|
5ea2b09dc3 | ||
|
|
5bfc2c4a9b | ||
|
|
8ec47c3894 | ||
|
|
f4423638a2 | ||
|
|
7b3444308b | ||
|
|
5ec9916817 | ||
|
|
be1a70299f | ||
|
|
8685aa371a | ||
|
|
0224faa184 | ||
|
|
8546e326cf | ||
|
|
9956fd2f04 | ||
|
|
4f669ca82f | ||
|
|
1cd3881aa8 | ||
|
|
e634513f7b | ||
|
|
7b6cde6ae3 | ||
|
|
18146fdf9e | ||
|
|
187d04e3a1 |
45
.github/workflows/base-image-build.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/deploy-demo.yml
vendored
@@ -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
|
||||
|
||||
1
.github/workflows/publish-atom.yaml
vendored
@@ -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
|
||||
|
||||
3
.github/workflows/publish-gitee.yaml
vendored
@@ -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
@@ -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: ./
|
||||
|
||||
@@ -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"},
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
docs/guide/use/dev/images/plugin-create.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
docs/guide/use/dev/images/plugin-edit.png
Normal file
|
After Width: | Height: | Size: 141 KiB |
BIN
docs/guide/use/dev/images/plugin-test1.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
docs/guide/use/dev/images/plugin-test2.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
docs/guide/use/dev/images/plugin-test3.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
19
docs/guide/use/dev/plugin.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# 插件开发
|
||||
|
||||
## 插件创建
|
||||
点击自定义插件按钮,填写插件基本信息
|
||||

|
||||
|
||||
创建成功后,会默认打开插件编辑页面,里面默认带有示例代码说明,可以在此基础上进行你的自定义开发
|
||||

|
||||
|
||||
## 插件测试
|
||||
|
||||
在流水线中添加插件任务
|
||||

|
||||
|
||||
配置插件任务参数
|
||||

|
||||
|
||||
点击运行,查看插件任务运行结果
|
||||

|
||||
@@ -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` 来临时关闭仅使用第三方登录模式,以使用密码登录。
|
||||
BIN
docs/guide/use/setting/images/user_valid_enable.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/guide/use/setting/images/user_valid_set.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
2
docs/guide/use/setting/oauth.md
Normal file
@@ -0,0 +1,2 @@
|
||||
# 第三方登录配置
|
||||
|
||||
11
docs/guide/use/setting/user-valid.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# 用户有效期功能
|
||||
|
||||
可以为用户设置有效期,超过有效期后,用户的流水线将停止运行
|
||||
|
||||
## 开启用户有效期功能
|
||||
|
||||

|
||||
|
||||
## 设置用户有效期
|
||||
|
||||

|
||||
BIN
docs/guide/use/synology/images/nettest.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -67,4 +67,31 @@
|
||||

|
||||
|
||||
## 6. 配置通知和自动运行
|
||||

|
||||

|
||||
|
||||
|
||||
## 三、 常见问题
|
||||
|
||||
### 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是多少
|
||||

|
||||
|
||||
2. 修改容器编排 docker-compose.yaml
|
||||
|
||||
```
|
||||
services:
|
||||
certd:
|
||||
...
|
||||
extra_hosts: # 放开这段注释
|
||||
- "你的dsm域名地址:172.xx.0.1" # 将xx替换成上面记住的数字
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -70,5 +70,5 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/publishlab/node-acme-client/issues"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -47,5 +47,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"prettier": "^2.8.8",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -31,5 +31,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -56,5 +56,5 @@
|
||||
"fetch"
|
||||
]
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -32,5 +32,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -64,5 +64,5 @@
|
||||
"typeorm": "^0.3.11",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -46,5 +46,5 @@
|
||||
"typeorm": "^0.3.11",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -57,5 +57,5 @@
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.4.2"
|
||||
},
|
||||
"gitHead": "a218cd0ffb16b658dd261e106cc0dbea63034756"
|
||||
"gitHead": "2c80c35b21b3f435e835167fca13db510bbc38a2"
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -28,4 +28,10 @@ export const Dicts = {
|
||||
{ label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true },
|
||||
],
|
||||
}),
|
||||
domainFromTypeDict: dict({
|
||||
data: [
|
||||
{ value: "manual", label: "手动" },
|
||||
{ value: "auto", label: "自动" },
|
||||
],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "选择",
|
||||
|
||||
38
packages/ui/certd-client/src/use/use-dialog.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
67
packages/ui/certd-client/src/views/certd/cert/domain/use.tsx
Normal 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();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 });
|
||||
|
||||
// 页面打开后获取列表数据
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
|
||||
@@ -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']">
|
||||
|
||||
@@ -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']">
|
||||
|
||||
@@ -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']">
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
LEGO_VERSION=4.30.1
|
||||
certd_plugin_loadmode=metadata
|
||||
certd_plugin_loadmode=dev
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('=========================================');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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服务不支持获取域名列表');
|
||||
}
|
||||
|
||||
/**
|
||||
* 中文转英文
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -39,6 +39,7 @@ export abstract class CertApplyBasePlugin extends CertApplyBaseConvertPlugin {
|
||||
vModel: "checked",
|
||||
},
|
||||
order: 100,
|
||||
maybeNeed: true,
|
||||
helper: "证书申请成功后是否发送通知,优先使用默认通知渠道",
|
||||
})
|
||||
successNotify = false;
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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'
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -1 +1 @@
|
||||
2
|
||||
01:00
|
||||
|
||||
@@ -1 +1 @@
|
||||
3
|
||||
01:32
|
||||
|
||||