Compare commits

...

100 Commits

Author SHA1 Message Date
xiaojunnuo
f17b08ddab v1.24.0 2024-08-25 14:27:41 +08:00
xiaojunnuo
893b853fd4 build: prepare to build 2024-08-25 14:26:34 +08:00
xiaojunnuo
15846eda85 chore: 2024-08-25 12:07:47 +08:00
xiaojunnuo
19ddf61127 chore: 2024-08-25 11:57:07 +08:00
xiaojunnuo
a7424e02f5 feat: 支持ECC类型 2024-08-25 11:56:15 +08:00
xiaojunnuo
d4092e4929 chore: 1 2024-08-25 03:27:38 +08:00
xiaojunnuo
62ef54c7c3 chore: 1 2024-08-25 03:14:07 +08:00
xiaojunnuo
85ae80c882 Merge remote-tracking branch 'origin/acme_sync' into v2
# Conflicts:
#	packages/core/acme-client/package.json
2024-08-25 03:01:13 +08:00
xiaojunnuo
a593056e79 feat: 支持google证书申请(需要使用代理) 2024-08-25 02:59:49 +08:00
xiaojunnuo
22a336370a fix: 修复执行日志没有清理的bug 2024-08-25 01:55:34 +08:00
xiaojunnuo
86ebbcb9bb chore: 1 2024-08-24 23:48:26 +08:00
xiaojunnuo
c28f3cdcf7 chore: 1 2024-08-24 01:05:06 +08:00
xiaojunnuo
41b9837582 fix: 修复ssh无法连接成功,无法执行命令的bug 2024-08-24 00:00:27 +08:00
xiaojunnuo
37eb762afe fix: 修复成功后跳过之后丢失腾讯云证书id的bug 2024-08-23 23:26:31 +08:00
xiaojunnuo
3345c145b8 fix: 部署到腾讯云cdn选择证书任务步骤限制只能选证书 2024-08-23 18:02:14 +08:00
xiaojunnuo
17ead547aa fix: 修复创建流水线后立即运行时报no id错误的bug
Closes https://github.com/certd/certd/issues/135
2024-08-23 17:41:02 +08:00
xiaojunnuo
e358a88696 fix: 修复重置密码参数配置后无效的bug 2024-08-23 17:16:52 +08:00
xiaojunnuo
968c4690a0 perf: 优化证书申请成功率 2024-08-23 13:15:06 +08:00
xiaojunnuo
453f1baa0b perf: email proxy 2024-08-23 11:35:34 +08:00
xiaojunnuo
14ab93dc2f chore: 1 2024-08-21 12:38:09 +08:00
xiaojunnuo
790bf11af0 perf: 优化dnspod的token id 说明 2024-08-21 10:36:10 +08:00
xiaojunnuo
95122e2860 fix: 修复使用代理的情况下申请证书失败的bug 2024-08-21 10:34:50 +08:00
xiaojunnuo
ab7a1673ff chore: plus 2024-08-21 08:36:03 +08:00
xiaojunnuo
db9d27468e chore: license说明 2024-08-14 21:24:12 +08:00
xiaojunnuo
746bb9d385 perf: 更新k8s底层api库 2024-08-14 15:10:55 +08:00
xiaojunnuo
7b451bbf6e perf: 优化成功后跳过的提示 2024-08-13 20:30:42 +08:00
xiaojunnuo
ffc4e71783 build: trigger build image 2024-08-06 11:39:34 +08:00
xiaojunnuo
7eb6d7d053 v1.23.1 2024-08-06 11:39:21 +08:00
xiaojunnuo
93b6431369 build: prepare to build 2024-08-06 11:38:30 +08:00
xiaojunnuo
d301ac6832 chore: 2024-08-06 11:37:46 +08:00
xiaojunnuo
1af19f0ac0 perf: 优化默认值设置 2024-08-06 11:32:25 +08:00
xiaojunnuo
24c7be2c9c perf: 优化插件字段的default value 2024-08-06 11:23:23 +08:00
xiaojunnuo
c3f04a80fd chore: pg 改成bigint 2024-08-06 11:10:33 +08:00
xiaojunnuo
bf6c5d690e chore: 权益 2024-08-06 10:32:07 +08:00
xiaojunnuo
7c92762f48 chore: k8s logger 2024-08-06 10:23:09 +08:00
xiaojunnuo
efacfd6b2c chore: k8s logger 2024-08-06 10:22:28 +08:00
xiaojunnuo
93559174c7 fix: 修复模糊查询无效的bug 2024-08-06 10:12:02 +08:00
xiaojunnuo
1b0ae8654f chore: 2024-08-06 09:12:25 +08:00
xiaojunnuo
d11a19ce59 chore: 2024-08-06 09:12:14 +08:00
xiaojunnuo
9a68b0fb61 chore: 2024-08-06 09:11:31 +08:00
xiaojunnuo
79bbdce1e1 chore: 2024-08-06 09:04:38 +08:00
xiaojunnuo
916ee4a089 chore: 2024-08-06 09:00:45 +08:00
xiaojunnuo
ac9313da38 build: trigger build image 2024-08-05 18:08:50 +08:00
xiaojunnuo
ed01ef1eb7 v1.23.0 2024-08-05 18:08:44 +08:00
xiaojunnuo
7ec2218c9f fix: 修复环境变量多个下划线不生效的bug 2024-08-05 17:47:56 +08:00
xiaojunnuo
e8ed97206b feat: use node 20 2024-08-05 16:27:01 +08:00
xiaojunnuo
c45d85e612 chore: 2024-08-05 16:24:16 +08:00
xiaojunnuo
b3ff0fd880 chore: 2024-08-05 16:19:28 +08:00
xiaojunnuo
2fbc7459e2 build: trigger build image 2024-08-05 16:10:45 +08:00
xiaojunnuo
fbf4959463 v1.22.9 2024-08-05 16:10:39 +08:00
xiaojunnuo
02bb0be06a chore: 2024-08-05 16:07:28 +08:00
xiaojunnuo
87e440ee2a perf: 优化定时任务 2024-08-05 16:00:04 +08:00
xiaojunnuo
2182dce07c chore: 修复pipelineid为空被注册任务 2024-08-05 15:08:24 +08:00
xiaojunnuo
3f0a10007c build: trigger build image 2024-08-05 13:20:09 +08:00
xiaojunnuo
67934cdebd v1.22.8 2024-08-05 13:19:57 +08:00
xiaojunnuo
6765a48706 chore: 2024-08-05 13:04:36 +08:00
xiaojunnuo
b4252033d5 perf: 优化pipeline删除时,删除其他history 2024-08-05 12:57:13 +08:00
xiaojunnuo
f78ae93eed perf: 修复删除历史记录没有删除log的bug,新增history管理页面,演示站点启动时不自动启动非管理员用户的定时任务 2024-08-05 12:49:44 +08:00
xiaojunnuo
0227155ab4 chore: 2024-08-04 22:46:08 +08:00
xiaojunnuo
330b84de33 build: trigger build image 2024-08-04 22:33:08 +08:00
xiaojunnuo
f47f86b669 v1.22.7 2024-08-04 22:32:13 +08:00
xiaojunnuo
95eeb93822 build: prepare to build 2024-08-04 22:30:36 +08:00
xiaojunnuo
367f807313 fix: 修复保存配置报id不能为空的bug 2024-08-04 22:25:51 +08:00
xiaojunnuo
a954629ff9 build: trigger build image 2024-08-04 02:55:27 +08:00
xiaojunnuo
3bbbc41062 v1.22.6 2024-08-04 02:53:50 +08:00
xiaojunnuo
bf63b0d73f build: prepare to build 2024-08-04 02:51:30 +08:00
xiaojunnuo
5362df55f4 chore: 1 2024-08-04 02:49:40 +08:00
xiaojunnuo
59897c4cea perf: 流水线支持名称模糊查询 2024-08-04 02:35:45 +08:00
xiaojunnuo
a9717b9a0d fix: 修复pg下pipeline title 类型问题 2024-08-04 00:04:55 +08:00
xiaojunnuo
680941af11 fix: 修复在相同的cron时偶尔无法触发定时任务的bug 2024-08-03 23:32:50 +08:00
xiaojunnuo
1cf8d4e5e7 chore: 2024-08-02 23:59:08 +08:00
xiaojunnuo
70ce6be0bf chore: 单元测试 2024-08-02 22:58:29 +08:00
xiaojunnuo
9187e87419 chore: 2024-08-02 09:29:26 +08:00
xiaojunnuo
6ed1e18c7d perf: 优化前置任务输出为空的提示 2024-08-02 09:26:54 +08:00
xiaojunnuo
8d27f07213 Merge remote-tracking branch 'origin/v2' into v2 2024-07-31 14:01:30 +08:00
xiaojunnuo
e4f4570b29 perf: 腾讯云clb支持更多大区选择 2024-07-31 14:01:06 +08:00
xiaojunnuo
d86fc9569a chore: 增加dns地址配置 2024-07-31 13:46:48 +08:00
xiaojunnuo
fa7a983bcb chore: 2024-07-26 23:48:15 +08:00
xiaojunnuo
9ac908ebee chore: 2024-07-26 23:44:57 +08:00
xiaojunnuo
6e594ee66e chore: 2024-07-26 23:43:41 +08:00
xiaojunnuo
c26d3e9c38 chore: 2024-07-26 23:38:51 +08:00
xiaojunnuo
5db5607faa chore: 2024-07-26 23:36:33 +08:00
xiaojunnuo
728f27e0a0 build: trigger build image 2024-07-26 23:16:17 +08:00
xiaojunnuo
3d8f329e2d v1.22.5 2024-07-26 23:15:20 +08:00
xiaojunnuo
351fb70d5d build: prepare to build 2024-07-26 23:14:05 +08:00
xiaojunnuo
b5cbeb9bde chore: 2024-07-26 23:13:50 +08:00
xiaojunnuo
e7e89b8de7 fix: 修复用户管理无法添加用户的bug 2024-07-26 23:07:01 +08:00
xiaojunnuo
225894d15c chore: 2024-07-26 22:48:18 +08:00
xiaojunnuo
64ba485b0f chore: 2024-07-26 22:46:05 +08:00
xiaojunnuo
3a666db36c chore: 2024-07-26 22:16:24 +08:00
xiaojunnuo
ce7e5a2461 chore: 2024-07-26 22:13:49 +08:00
xiaojunnuo
b22f94b079 chore: 2024-07-26 22:11:41 +08:00
xiaojunnuo
3408465df6 chore: 2024-07-26 22:01:42 +08:00
xiaojunnuo
e97dfb456b chore: 2024-07-26 21:42:52 +08:00
xiaojunnuo
439c6c8b6c chore: 2024-07-26 21:42:25 +08:00
xiaojunnuo
afa2b0307a chore: 2024-07-26 21:40:32 +08:00
xiaojunnuo
56867fa777 chore: 2024-07-26 21:37:34 +08:00
xiaojunnuo
9c2e33fa39 chore: 2024-07-26 21:36:25 +08:00
xiaojunnuo
2ca72f838b chore: 2024-07-26 20:56:48 +08:00
GitHub Actions Bot
e5edfbfa6d 🔱: [acme] sync upgrade with 6 commits [trident-sync]
Bump v5.4.0
Bump dependencies
Retry HTTP requests on server errors or when rate limited
Forgot to refresh directory timestamp after successful get
Add utility method tests
2024-07-16 19:24:08 +00:00
623 changed files with 3606 additions and 29756 deletions

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: get_certd_version
id: get_certd_version
@@ -33,6 +33,17 @@ jobs:
const pkg = JSON.parse(jsonContent)
console.log("certd_version:",pkg.version);
return pkg.version
# - name: Use Node.js
# uses: actions/setup-node@v4
# with:
# node-version: 18
# cache: 'npm'
# working-directory: ./packages/ui/certd-client
- run: |
npm install -g pnpm
pnpm install
npm run build
working-directory: ./packages/ui/certd-client
- name: Set up QEMU
uses: docker/setup-qemu-action@v3

55
.github/workflows/deploy-demo.yml vendored Normal file
View File

@@ -0,0 +1,55 @@
name: deploy-demo
on:
push:
branches: ['v2']
paths:
- "deploy.trigger"
workflow_run:
workflows: [ "build-image" ]
types:
- completed
# schedule:
# - # 国际时间 19:17 执行北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
jobs:
deploy-certd-demo:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: get_certd_version
id: get_certd_version
uses: actions/github-script@v6
with:
result-encoding: string
script: |
const fs = require('fs');
const path = require('path');
const jsonFilePath = "./packages/ui/certd-server/package.json";
const jsonContent = fs.readFileSync(jsonFilePath, 'utf-8');
const pkg = JSON.parse(jsonContent)
console.log("certd_version:",pkg.version);
return pkg.version
- uses: GuillaumeFalourd/wait-sleep-action@v1
with:
time: '10' # for 60 seconds
- name: Send HTTP request
id: request
uses: tyrrrz/action-http-request@master
with:
url: http://flow-openapi.aliyun.com/pipeline/webhook/lzCzlGrLCOHQaTMMt0mG
method: POST
headers: |
Content-Type: application/json
body: |
{
"CERTD_VERSION": "${{steps.get_certd_version.outputs.result}}"
}
retry-count: 3
retry-delay: 5000

View File

@@ -3,6 +3,90 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes
* 部署到腾讯云cdn选择证书任务步骤限制只能选证书 ([3345c14](https://github.com/certd/certd/commit/3345c145b802170f75a098a35d0c4b8312efcd17))
* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7))
* 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/certd/certd/commit/17ead547aab25333603980304aa3aad3db1f73d5))
* 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/certd/certd/commit/95122e28609333f4df55c266e5434897954c0fb3))
* 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5))
* 修复重置密码参数配置后无效的bug ([e358a88](https://github.com/certd/certd/commit/e358a8869696578687306e4cd0dcda53f898fe13))
* 修复ssh无法连接成功无法执行命令的bug ([41b9837](https://github.com/certd/certd/commit/41b9837582323fb400ef8525ce65e8b37ad4b36f))
### Features
* 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473))
* 支持google证书申请需要使用代理 ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe))
### Performance Improvements
* 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd))
* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b))
* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345))
* 优化dnspod的token id 说明 ([790bf11](https://github.com/certd/certd/commit/790bf11af06d6264ef74bc1bb919661f0354239a))
* email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f))
## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Bug Fixes
* 修复模糊查询无效的bug ([9355917](https://github.com/certd/certd/commit/93559174c780173f0daec7cdbd1f72f8d5c504d5))
### Performance Improvements
* 优化插件字段的default value ([24c7be2](https://github.com/certd/certd/commit/24c7be2c9cb39c14f7a97b674127c88033280b02))
* 优化默认值设置 ([1af19f0](https://github.com/certd/certd/commit/1af19f0ac053fe109782882964533636b5969d6b))
# [1.23.0](https://github.com/certd/certd/compare/v1.22.9...v1.23.0) (2024-08-05)
### Bug Fixes
* 修复环境变量多个下划线不生效的bug ([7ec2218](https://github.com/certd/certd/commit/7ec2218c9fee5bee2bf0aa31f3e3a4301575f247))
### Features
* use node 20 ([e8ed972](https://github.com/certd/certd/commit/e8ed97206bf28e83f942db2ef4ea07fa76fd3567))
## [1.22.9](https://github.com/certd/certd/compare/v1.22.8...v1.22.9) (2024-08-05)
### Performance Improvements
* 优化定时任务 ([87e440e](https://github.com/certd/certd/commit/87e440ee2a8b10dc571ce619f28bc83c1e5eb147))
## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05)
### Performance Improvements
* 修复删除历史记录没有删除log的bug新增history管理页面演示站点启动时不自动启动非管理员用户的定时任务 ([f78ae93](https://github.com/certd/certd/commit/f78ae93eedfe214008c3d071ca3d77c962137a64))
* 优化pipeline删除时删除其他history ([b425203](https://github.com/certd/certd/commit/b4252033d56a9ad950f3e204ff021497c3978015))
## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04)
### Bug Fixes
* 修复保存配置报id不能为空的bug ([367f807](https://github.com/certd/certd/commit/367f80731396003416665c22853dfbc09c2c03a0))
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
### Bug Fixes
* 修复在相同的cron时偶尔无法触发定时任务的bug ([680941a](https://github.com/certd/certd/commit/680941af119619006b592e3ab6fb112cb5556a8b))
* 修复pg下pipeline title 类型问题 ([a9717b9](https://github.com/certd/certd/commit/a9717b9a0df7b5a64d4fe03314fecad4f59774cc))
### Performance Improvements
* 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7))
* 腾讯云clb支持更多大区选择 ([e4f4570](https://github.com/certd/certd/commit/e4f4570b29f26c60f1ee9660a4c507cbeaba3d7e))
* 优化前置任务输出为空的提示 ([6ed1e18](https://github.com/certd/certd/commit/6ed1e18c7d9c46d964ecc6abc90f3908297b7632))
## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26)
### Bug Fixes
* 修复用户管理无法添加用户的bug ([e7e89b8](https://github.com/certd/certd/commit/e7e89b8de7386e84c0d6b8e217e2034909657d68))
## [1.22.4](https://github.com/certd/certd/compare/v1.22.3...v1.22.4) (2024-07-26)
### Performance Improvements

30
LICENSE.md Normal file
View File

@@ -0,0 +1,30 @@
# Certd Open Source License
- This project is licensed under the **GNU Affero General Public License (AGPL)** with the following additional terms.
- 本项目遵循 GNU Affero General Public LicenseAGPL并附加以下条款。
## 1. License Terms ( 许可证条款 )
1. **Freedom to Use** (自由使用)
- You are free to use, copy, modify, and distribute the source code of this project for personal or organizational use, provided that you comply with the terms of this license.
- 您可以自由使用、复制、修改和分发本项目的源代码,前提是您遵循本许可证的条款。
2. **Modification for Personal Use** (个人使用的修改)
- Individuals and companies are allowed to modify the project according to their needs for non-commercial purposes. However, modifications to the logo, copyright information, or any code related to licensing are strictly prohibited.
- 个人和公司允许根据自身需求对本项目进行修改以供非商业用途。但任何对logo、版权信息或与许可相关代码的修改都是严格禁止的。
3. **Commercial Authorization** (商业授权)
- If you wish to make any form of monetary gain from this project, you must first obtain commercial authorization from the original author. Users should contact the author directly to negotiate the relevant licensing terms.
- 如果您希望从本项目获得任何形式的经济收益,您必须首先从原作者处获得商业授权,用户应直接与作者联系,以协商相关许可条款。
4. **Retention of Rights** (保留权利)
- All rights, title, and interest in the project remain with the original author.
- 本项目的所有权利、标题和利益仍归原作者所有。
## 2. As a contributor ( 作为贡献者 )
- you should agree that your contributed code:
- 您应同意您贡献的代码:
1. - The original author can adjust the open-source agreement to be more strict or relaxed.
- 原作者可以调整开源协议以使其更严格或更宽松。
2. - Can be used for commercial purposes.
- 可用于商业用途。

View File

@@ -1,10 +1,21 @@
# CertD
# Certd
CertD 是一个免费全自动申请和自动部署更新SSL证书的工具。
后缀D取自linux守护进程的命名风格意为证书守护进程。
Certd 是一个免费全自动申请和自动部署更新SSL证书的工具。
后缀d取自linux守护进程的命名风格意为证书守护进程。
关键字:证书自动申请、证书自动更新、证书自动续期、证书自动续签
************************
支持开源,为爱发电,我已入驻爱发电
https://afdian.com/a/greper
发电权益:
1. 可加入发电专属群(先加我好友,发送发电截图,我拉你进群)
2. 你的需求优先实现
3. 可以获得作者一对一技术支持
4. 更多权益陆续增加中...
************************
## 一、特性
本项目不仅支持证书申请过程自动化,还可以自动化部署更新证书,让你的证书永不过期。
@@ -22,7 +33,7 @@ CertD 是一个免费全自动申请和自动部署更新SSL证书的工具。
https://certd.handsfree.work/
> 注意数据将不定期清理,生产使用请自行部署
> 注意数据将不定期清理,不定期停止定时任务,生产使用请自行部署
> 包含敏感信息,务必自己本地部署进行生产使用
## 三、使用教程
@@ -131,6 +142,7 @@ http://your_server_ip:7001
* [Cloudflare](./doc/cf/cf.md)
* [腾讯云](./doc/tencent/tencent.md)
* [windows主机](./doc/host/host.md)
* [google证书](./doc/google/google.md)
## 八、问题处理
@@ -169,26 +181,37 @@ docker compose up -d
</p>
## 十、捐赠
媳妇儿说:“一天到晚搞开源,也不管管老婆孩子!😡😡😡”
拜托各位捐赠支持一下,让媳妇儿开心开心,我也能有更多时间进行开源项目,感谢🙏🙏🙏
<p align="center">
<img height="380" src="./doc/images/donate.png">
</p>
支持开源,为爱发电,我已入驻爱发电
https://afdian.com/a/greper
发电权益:
1. 可加入发电专属群(先加我好友,发送发电截图,我拉你进群)
2. 你的需求优先实现
3. 可以获得作者一对一技术支持
4. 更多权益陆续增加中...
## 十一、贡献代码
[贡献插件教程](./plugin.md)
1. [贡献插件教程](./plugin.md)
2. 作为贡献者,代表您同意您贡献的代码如下许可:
1. 可以调整开源协议以使其更严格或更宽松。
2. 可以用于商业用途。
## 十二、 开源许可
* 本项目遵循 GNU Affero General Public LicenseAGPL开源协议。
* 允许个人和公司使用、复制、修改和分发本项目,禁止任何形式的商业用途
* 未获得商业授权情况下禁止任何对logo、版权信息及授权许可相关代码的修改。
* 如需商业授权,请联系作者。
## 十、我的其他项目求Star
## 十、我的其他项目求Star
* [袖手GPT](https://ai.handsfree.work/) ChatGPT国内可用无需FQ每日免费额度
* [fast-crud](https://gitee.com/fast-crud/fast-crud/) 基于vue3的crud快速开发框架
* [dev-sidecar](https://github.com/docmirror/dev-sidecar/) 直连访问github工具无需FQ解决github无法访问的问题
## 十、更新日志
## 十、更新日志
更新日志:[CHANGELOG](./CHANGELOG.md)

View File

@@ -1 +1 @@
7
11:39

1
deploy.trigger Normal file
View File

@@ -0,0 +1 @@
5

37
doc/google/google.md Normal file
View File

@@ -0,0 +1,37 @@
# google证书申请教程
## 1、启用API
打开如下链接,启用 API
https://console.cloud.google.com/apis/library/publicca.googleapis.com
打开该链接后点击“启用”随后等待右侧出现“API已启用”则可以关闭该页。
## 2、 申请Key
随后打开“Google Cloud Shell”在右上角点击激活CloudShell图标
等待分配完成后在 Shell 窗口内输入如下命令:
```shell
gcloud beta publicca external-account-keys create
```
此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。
执行完成后会返回类似如下输出;注意不要在没有收到 Google 的邮件时执行该命令,会返回命令不存在。
```shell
Created an external account key
[b64MacKey: xxxxxxxxxxxxx
keyId: xxxxxxxxx]
```
记录以上信息备用
## 3、 创建证书流水线
选择证书提供商为google 开启使用代理
## 4、 将key信息作为EAB授权信息
google证书需要EAB授权 使用第二步中的 keyId 和 b64MacKey 信息创建一条EAB授权记录
## 5、 其他就跟正常申请证书一样了

View File

@@ -11,9 +11,16 @@ services:
ports: # 端口映射
# ↓↓↓↓ ----------------------------------------------------------3、如果端口有冲突可以修改第一个7001为其他不冲突的端口号【可选】
- "7001:7001"
dns:
# 如果出现getaddrinfo ENOTFOUND等错误可以尝试修改或注释dns配置
- 223.5.5.5
- 223.6.6.6
# ↓↓↓↓ ----------------------------------------------------------如果你服务器部署在国外可以用8.8.8.8替换上面的dns【可选】
# - 8.8.8.8
# - 8.8.4.4
environment: # 环境变量
- TZ=Asia/Shanghai
- certd_system_resetAdminPassword=false
- certd_system_resetAdminPasswd=false
# ↑↑↑↑↑---------------------------4、如果忘记管理员密码可以设置为true重启之后管理员密码将改成123456然后请及时修改回false【可选】
- certd_cron_immediateTriggerOnce=false
# ↑↑↑↑↑---------------------------5、如果设置为true启动后所有配置了cron的流水线任务都将被立即触发一次【可选】

View File

@@ -9,5 +9,5 @@
}
},
"npmClient": "pnpm",
"version": "1.22.4"
"version": "1.24.0"
}

View File

@@ -12,10 +12,11 @@
"scripts": {
"start": "lerna bootstrap --hoist",
"i-all": "lerna link && lerna exec npm install ",
"publish": "npm run prepublishOnly1 && lerna publish --conventional-commits --create-release github && npm run afterpublishOnly",
"afterpublishOnly": "",
"prepublishOnly1": "npm run check && npm run before-build && lerna run build ",
"before-build": "cd ./packages/core/acme-client && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"publish": "npm run prepublishOnly2 && lerna publish --conventional-commits --create-release github && npm run afterpublishOnly",
"afterpublishOnly": "time /t >build.trigger && git add ./build.trigger && git commit -m \"build: trigger build image\" && TIMEOUT /T 10 && git push",
"prepublishOnly1": "npm run check && lerna run build ",
"prepublishOnly2": "npm run check && npm run before-build && lerna run build ",
"before-build": "cd ./packages/core/pipeline && time /t >build.md && git add ./build.md && git commit -m \"build: prepare to build\"",
"deploy1": "node --experimental-json-modules deploy.js ",
"check": "node --experimental-json-modules publish-check.js",
"init": "lerna run build"

View File

@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/publishlab/node-acme-client/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes
* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/publishlab/node-acme-client/commit/37eb762afe25c5896b75dee25f32809f8426e7b7))
* 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/publishlab/node-acme-client/commit/17ead547aab25333603980304aa3aad3db1f73d5))
* 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/publishlab/node-acme-client/commit/95122e28609333f4df55c266e5434897954c0fb3))
### Features
* 支持google证书申请需要使用代理 ([a593056](https://github.com/publishlab/node-acme-client/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe))
### Performance Improvements
* 优化证书申请成功率 ([968c469](https://github.com/publishlab/node-acme-client/commit/968c4690a07f69c08dcb3d3a494da4e319627345))
## [1.22.6](https://github.com/publishlab/node-acme-client/compare/v1.22.5...v1.22.6) (2024-08-03)
**Note:** Version bump only for package @certd/acme-client
## [1.22.4](https://github.com/publishlab/node-acme-client/compare/v1.22.3...v1.22.4) (2024-07-26)
### Performance Improvements
@@ -106,10 +126,11 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
# Changelog
## v5.4.0
## v5.4.0 (2024-07-16)
* `added` Directory URLs for [Google](https://cloud.google.com/certificate-manager/docs/overview) ACME provider
* `fixed` Invalidate ACME directory cache after 24 hours
* `fixed` Invalidate ACME provider directory cache after 24 hours
* `fixed` Retry HTTP requests on server errors or when rate limited - [#89](https://github.com/publishlab/node-acme-client/issues/89)
## v5.3.1 (2024-05-22)
@@ -119,7 +140,7 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
## v5.3.0 (2024-02-05)
* `added` Support and tests for satisfying `tls-alpn-01` challenges
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR handling
* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge
* Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
* This change is not considered breaking since the previous behavior was incorrect

View File

@@ -3,7 +3,7 @@
"description": "Simple and unopinionated ACME client",
"private": false,
"author": "nmorsman",
"version": "1.22.4",
"version": "1.24.0",
"main": "src/index.js",
"types": "types/index.d.ts",
"license": "MIT",
@@ -16,24 +16,24 @@
"types"
],
"dependencies": {
"@peculiar/x509": "^1.10.0",
"@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5",
"axios": "^1.7.2",
"debug": "^4.1.1",
"debug": "^4.3.5",
"https-proxy-agent": "^7.0.4",
"node-forge": "^1.3.1"
},
"devDependencies": {
"@types/node": "^20.12.12",
"@types/node": "^20.14.10",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.2",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1",
"jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.4.0",
"mocha": "^10.6.0",
"nock": "^13.5.4",
"tsd": "^0.31.0",
"tsd": "^0.31.1",
"typescript": "^4.8.4",
"uuid": "^8.3.2"
},

View File

@@ -3,6 +3,7 @@
*/
const util = require('./util');
const { log } = require('./logger');
/**
* AcmeApi
@@ -17,6 +18,21 @@ class AcmeApi {
this.accountUrl = accountUrl;
}
getLocationFromHeader(resp) {
let locationUrl = resp.headers.location;
const mapping = this.http.urlMapping;
if (mapping.mappings) {
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (const key in mapping.mappings) {
const url = mapping.mappings[key];
if (locationUrl.indexOf(url) > -1) {
locationUrl = locationUrl.replace(url, key);
}
}
}
return locationUrl;
}
/**
* Get account URL
*
@@ -103,7 +119,7 @@ class AcmeApi {
/* Set account URL */
if (resp.headers.location) {
this.accountUrl = resp.headers.location;
this.accountUrl = this.getLocationFromHeader(resp);
}
return resp;

View File

@@ -137,9 +137,13 @@ module.exports = async (client, userOpts) => {
}
else {
log(`[auto] [${d}] Running challenge verification`);
await client.verifyChallenge(authz, challenge);
try {
await client.verifyChallenge(authz, challenge);
}
catch (e) {
log(`[auto] [${d}] challenge verification threw error: ${e.message}`);
}
}
/* Complete challenge and wait for valid status */
log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`);
await client.completeChallenge(challenge);
@@ -170,11 +174,41 @@ module.exports = async (client, userOpts) => {
throw e;
}
};
const domainSets = [];
const challengePromises = authorizations.map((authz) => async () => {
await challengeFunc(authz);
authorizations.forEach((authz) => {
const d = authz.identifier.value;
let setd = false;
// eslint-disable-next-line no-restricted-syntax
for (const group of domainSets) {
if (!group[d]) {
group[d] = authz;
setd = true;
}
}
if (!setd) {
const group = {};
group[d] = authz;
domainSets.push(group);
}
});
const allChallengePromises = [];
// eslint-disable-next-line no-restricted-syntax
for (const domainSet of domainSets) {
const challengePromises = [];
// eslint-disable-next-line guard-for-in,no-restricted-syntax
for (const domain in domainSet) {
const authz = domainSet[domain];
challengePromises.push(async () => {
log(`[auto] [${domain}] Starting challenge`);
await challengeFunc(authz);
});
}
allChallengePromises.push(challengePromises);
}
log(`[auto] challengeGroups:${allChallengePromises.length}`);
function runAllPromise(tasks) {
let promise = Promise.resolve();
tasks.forEach((task) => {
@@ -195,9 +229,18 @@ module.exports = async (client, userOpts) => {
}
try {
log('开始challenge');
await runPromisePa(challengePromises);
log(`开始challenge,共${allChallengePromises.length}`);
let i = 0;
// eslint-disable-next-line no-restricted-syntax
for (const challengePromises of allChallengePromises) {
i += 1;
log(`开始第${i}`);
if (opts.signal && opts.signal.aborted) {
throw new Error('用户取消');
}
// eslint-disable-next-line no-await-in-loop
await runPromisePa(challengePromises);
}
log('challenge结束');
// log('[auto] Waiting for challenge valid status');

View File

@@ -3,10 +3,14 @@
*/
const axios = require('axios');
const { parseRetryAfterHeader } = require('./util');
const { log } = require('./logger');
const pkg = require('./../package.json');
const { AxiosError } = axios;
/**
* Instance
* Defaults
*/
const instance = axios.create();
@@ -19,6 +23,9 @@ instance.defaults.acmeSettings = {
httpChallengePort: 80,
httpsChallengePort: 443,
tlsAlpnChallengePort: 443,
retryMaxAttempts: 5,
retryDefaultDelay: 5,
};
// instance.defaults.proxy = {
// host: '192.168.34.139',
@@ -33,6 +40,85 @@ instance.defaults.acmeSettings = {
instance.defaults.adapter = 'http';
/**
* Retry requests on server errors or when rate limited
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.6
*/
function isRetryableError(error) {
return (error.code !== 'ECONNABORTED')
&& (error.code !== 'ERR_NOCK_NO_MATCH')
&& (!error.response
|| (error.response.status === 429)
|| ((error.response.status >= 500) && (error.response.status <= 599)));
}
/* https://github.com/axios/axios/blob/main/lib/core/settle.js */
function validateStatus(response) {
const validator = response.config.retryValidateStatus;
if (!response.status || !validator || validator(response.status)) {
return response;
}
throw new AxiosError(
`Request failed with status code ${response.status}`,
(Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
response.config,
response.request,
response,
);
}
/* Pass all responses through the error interceptor */
instance.interceptors.request.use((config) => {
if (!('retryValidateStatus' in config)) {
config.retryValidateStatus = config.validateStatus;
}
config.validateStatus = () => false;
return config;
});
/* Handle request retries if applicable */
instance.interceptors.response.use(null, async (error) => {
const { config, response } = error;
if (!config) {
return Promise.reject(error);
}
/* Pick up errors we want to retry */
if (isRetryableError(error)) {
const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings;
config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1;
if (config.retryAttempt <= retryMaxAttempts) {
const code = response ? `HTTP ${response.status}` : error.code;
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
/* Attempt to parse Retry-After header, fallback to default delay */
let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
if (retryAfter > 0) {
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`);
}
else {
retryAfter = (retryDefaultDelay * config.retryAttempt);
log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`);
}
/* Wait and retry the request */
await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); });
return instance(config);
}
}
/* Validate and return response */
return validateStatus(response);
});
/**
* Export instance
*/

View File

@@ -300,7 +300,8 @@ class AcmeClient {
}
/* Add URL to response */
resp.data.url = resp.headers.location;
resp.data.url = this.api.getLocationFromHeader(resp);
return resp.data;
}
@@ -490,6 +491,9 @@ class AcmeClient {
const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);
const verifyFn = async () => {
if (this.opts.signal && this.opts.signal.aborted) {
throw new Error('用户取消');
}
await verify[challenge.type](authz, challenge, keyAuthorization);
};
@@ -513,6 +517,9 @@ class AcmeClient {
*/
async completeChallenge(challenge) {
if (this.opts.signal && this.opts.signal.aborted) {
throw new Error('用户取消');
}
const resp = await this.api.completeChallenge(challenge.url, {});
return resp.data;
}
@@ -550,6 +557,10 @@ class AcmeClient {
}
const verifyFn = async (abort) => {
if (this.opts.signal && this.opts.signal.aborted) {
throw new Error('用户取消');
}
const resp = await this.api.apiRequest(item.url, null, [200]);
/* Verify status */

View File

@@ -10,6 +10,7 @@
const net = require('net');
const { promisify } = require('util');
const forge = require('node-forge');
const { createPrivateEcdsaKey, getPublicKey } = require('./index');
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
@@ -378,13 +379,17 @@ function formatCsrAltNames(altNames) {
* }, certificateKey);
*/
exports.createCsr = async (data, key = null) => {
if (!key) {
exports.createCsr = async (data, keyType = null) => {
let key = null;
if (keyType === 'ec') {
key = await createPrivateEcdsaKey();
}
else {
key = await createPrivateKey(data.keySize);
}
else if (!Buffer.isBuffer(key)) {
key = Buffer.from(key);
}
// else if (!Buffer.isBuffer(key)) {
// key = Buffer.from(key);
// }
if (typeof data.altNames === 'undefined') {
data.altNames = [];
@@ -396,6 +401,8 @@ exports.createCsr = async (data, key = null) => {
const privateKey = forge.pki.privateKeyFromPem(key);
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
csr.publicKey = publicKey;
// const privateKey = key;
// csr.publicKey = getPublicKey(key);
/* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
if (data.commonName && !data.altNames.includes(data.commonName)) {

View File

@@ -55,7 +55,7 @@ class HttpClient {
*/
async request(url, method, opts = {}) {
if (this.urlMapping && this.urlMapping.enabled === true && this.urlMapping.mappings) {
if (this.urlMapping && this.urlMapping.mappings) {
// eslint-disable-next-line no-restricted-syntax
for (const key in this.urlMapping.mappings) {
if (url.includes(key)) {
@@ -93,9 +93,11 @@ class HttpClient {
*/
async getDirectory() {
const age = (Math.floor(Date.now() / 1000) - this.directoryTimestamp);
const now = Math.floor(Date.now() / 1000);
const age = (now - this.directoryTimestamp);
if (!this.directoryCache || (age > this.directoryMaxAge)) {
log(`Refreshing ACME directory, age: ${age}`);
const resp = await this.request(this.directoryUrl, 'get');
if (resp.status >= 400) {
@@ -107,6 +109,7 @@ class HttpClient {
}
this.directoryCache = resp.data;
this.directoryTimestamp = now;
}
return this.directoryCache;
@@ -131,7 +134,7 @@ class HttpClient {
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
*
* @returns {Promise<string>} nonce
* @returns {Promise<string>} Nonce
*/
async getNonce() {

View File

@@ -84,9 +84,12 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
}
/**
* Parse URLs from link header
* Parse URLs from Link header
*
* @param {string} header Link header contents
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
*
* @param {string} header Header contents
* @param {string} rel Link relation, default: `alternate`
* @returns {string[]} Array of URLs
*/
@@ -102,6 +105,37 @@ function parseLinkHeader(header, rel = 'alternate') {
return results.filter((r) => r);
}
/**
* Parse date or duration from Retry-After header
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
*
* @param {string} header Header contents
* @returns {number} Retry duration in seconds
*/
function parseRetryAfterHeader(header) {
const sec = parseInt(header, 10);
const date = new Date(header);
/* Seconds into the future */
if (Number.isSafeInteger(sec) && (sec > 0)) {
return sec;
}
/* Future date string */
if (date instanceof Date && !Number.isNaN(date)) {
const now = new Date();
const diff = Math.ceil((date.getTime() - now.getTime()) / 1000);
if (diff > 0) {
return diff;
}
}
return 0;
}
/**
* Find certificate chain with preferred issuer common name
* - If issuer is found in multiple chains, the closest to root wins
@@ -161,14 +195,16 @@ function findCertificateChainForIssuer(chains, issuer) {
function formatResponseError(resp) {
let result;
if (resp.data.error) {
result = resp.data.error.detail || resp.data.error;
}
else {
result = resp.data.detail || JSON.stringify(resp.data);
if (resp.data) {
if (resp.data.error) {
result = resp.data.error.detail || resp.data.error;
}
else {
result = resp.data.detail || JSON.stringify(resp.data);
}
}
return result.replace(/\n/g, '');
return (result || '').replace(/\n/g, '');
}
/**
@@ -296,6 +332,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
module.exports = {
retry,
parseLinkHeader,
parseRetryAfterHeader,
findCertificateChainForIssuer,
formatResponseError,
getAuthoritativeDnsResolver,

View File

@@ -12,33 +12,12 @@ const pkg = require('./../package.json');
describe('http', () => {
let testClient;
const endpoint = `http://${uuid()}.example.com`;
const defaultUserAgent = `node-${pkg.name}/${pkg.version}`;
const customUserAgent = 'custom-ua-123';
const primaryEndpoint = `http://${uuid()}.example.com`;
const defaultUaEndpoint = `http://${uuid()}.example.com`;
const customUaEndpoint = `http://${uuid()}.example.com`;
/**
* HTTP mocking
*/
before(() => {
const defaultUaOpts = { reqheaders: { 'User-Agent': defaultUserAgent } };
const customUaOpts = { reqheaders: { 'User-Agent': customUserAgent } };
nock(primaryEndpoint)
.persist().get('/').reply(200, 'ok');
nock(defaultUaEndpoint, defaultUaOpts)
.persist().get('/').reply(200, 'ok');
nock(customUaEndpoint, customUaOpts)
.persist().get('/').reply(200, 'ok');
});
after(() => {
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
afterEach(() => {
nock.cleanAll();
});
/**
@@ -54,7 +33,8 @@ describe('http', () => {
*/
it('should http get', async () => {
const resp = await testClient.request(primaryEndpoint, 'get');
nock(endpoint).get('/').reply(200, 'ok');
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
@@ -66,28 +46,76 @@ describe('http', () => {
*/
it('should request using default user-agent', async () => {
const resp = await testClient.request(defaultUaEndpoint, 'get');
nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
});
it('should not request using custom user-agent', async () => {
await assert.isRejected(testClient.request(customUaEndpoint, 'get'));
it('should reject using custom user-agent', async () => {
nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = customUserAgent;
await assert.isRejected(testClient.request(endpoint, 'get'));
});
it('should request using custom user-agent', async () => {
nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = customUserAgent;
const resp = await testClient.request(customUaEndpoint, 'get');
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
});
it('should not request using default user-agent', async () => {
axios.defaults.headers.common['User-Agent'] = customUserAgent;
await assert.isRejected(testClient.request(defaultUaEndpoint, 'get'));
it('should reject using default user-agent', async () => {
nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
await assert.isRejected(testClient.request(endpoint, 'get'));
});
/**
* Retry on HTTP errors
*/
it('should retry on 429 rate limit', async () => {
let rateLimitCount = 0;
nock(endpoint).persist().get('/').reply(() => {
rateLimitCount += 1;
if (rateLimitCount < 3) {
return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }];
}
return [200, 'ok'];
});
assert.strictEqual(rateLimitCount, 0);
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
assert.strictEqual(rateLimitCount, 3);
});
it('should retry on 5xx server error', async () => {
let serverErrorCount = 0;
nock(endpoint).persist().get('/').reply(() => {
serverErrorCount += 1;
return [500, 'Internal Server Error', { 'Retry-After': 1 }];
});
assert.strictEqual(serverErrorCount, 0);
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 500);
assert.strictEqual(serverErrorCount, 4);
});
});

View File

@@ -0,0 +1,145 @@
/**
* Utility method tests
*/
const dns = require('dns').promises;
const fs = require('fs').promises;
const path = require('path');
const { assert } = require('chai');
const util = require('./../src/util');
const { readCertificateInfo } = require('./../src/crypto');
describe('util', () => {
const testCertPath1 = path.join(__dirname, 'fixtures', 'certificate.crt');
const testCertPath2 = path.join(__dirname, 'fixtures', 'letsencrypt.crt');
it('retry()', async () => {
let attempts = 0;
const backoffOpts = {
min: 100,
max: 500,
};
await assert.isRejected(util.retry(() => {
throw new Error('oops');
}, backoffOpts));
const r = await util.retry(() => {
attempts += 1;
if (attempts < 3) {
throw new Error('oops');
}
return 'abc';
}, backoffOpts);
assert.strictEqual(r, 'abc');
assert.strictEqual(attempts, 3);
});
it('parseLinkHeader()', () => {
const r1 = util.parseLinkHeader('<https://example.com/a>;rel="alternate"');
assert.isArray(r1);
assert.strictEqual(r1.length, 1);
assert.strictEqual(r1[0], 'https://example.com/a');
const r2 = util.parseLinkHeader('<https://example.com/b>;rel="test"');
assert.isArray(r2);
assert.strictEqual(r2.length, 0);
const r3 = util.parseLinkHeader('<http://example.com/c>; rel="test"', 'test');
assert.isArray(r3);
assert.strictEqual(r3.length, 1);
assert.strictEqual(r3[0], 'http://example.com/c');
const r4 = util.parseLinkHeader(`<https://example.com/a>; rel="alternate",
<https://example.com/x>; rel="nope",
<https://example.com/b>;rel="alternate",
<https://example.com/c>; rel="alternate"`);
assert.isArray(r4);
assert.strictEqual(r4.length, 3);
assert.strictEqual(r4[0], 'https://example.com/a');
assert.strictEqual(r4[1], 'https://example.com/b');
assert.strictEqual(r4[2], 'https://example.com/c');
});
it('parseRetryAfterHeader()', () => {
const r1 = util.parseRetryAfterHeader('');
assert.strictEqual(r1, 0);
const r2 = util.parseRetryAfterHeader('abcdef');
assert.strictEqual(r2, 0);
const r3 = util.parseRetryAfterHeader('123');
assert.strictEqual(r3, 123);
const r4 = util.parseRetryAfterHeader('123.456');
assert.strictEqual(r4, 123);
const r5 = util.parseRetryAfterHeader('-555');
assert.strictEqual(r5, 0);
const r6 = util.parseRetryAfterHeader('Wed, 21 Oct 2015 07:28:00 GMT');
assert.strictEqual(r6, 0);
const now = new Date();
const future = new Date(now.getTime() + 123000);
const r7 = util.parseRetryAfterHeader(future.toUTCString());
assert.isTrue(r7 > 100);
});
it('findCertificateChainForIssuer()', async () => {
const certs = [
(await fs.readFile(testCertPath1)).toString(),
(await fs.readFile(testCertPath2)).toString(),
];
const r1 = util.findCertificateChainForIssuer(certs, 'abc123');
const r2 = util.findCertificateChainForIssuer(certs, 'example.com');
const r3 = util.findCertificateChainForIssuer(certs, 'E6');
[r1, r2, r3].forEach((r) => {
assert.isString(r);
assert.isNotEmpty(r);
});
assert.strictEqual(readCertificateInfo(r1).issuer.commonName, 'example.com');
assert.strictEqual(readCertificateInfo(r2).issuer.commonName, 'example.com');
assert.strictEqual(readCertificateInfo(r3).issuer.commonName, 'E6');
});
it('formatResponseError()', () => {
const e1 = util.formatResponseError({ data: { error: 'aaa' } });
assert.strictEqual(e1, 'aaa');
const e2 = util.formatResponseError({ data: { error: { detail: 'bbb' } } });
assert.strictEqual(e2, 'bbb');
const e3 = util.formatResponseError({ data: { detail: 'ccc' } });
assert.strictEqual(e3, 'ccc');
const e4 = util.formatResponseError({ data: { a: 123 } });
assert.strictEqual(e4, '{"a":123}');
const e5 = util.formatResponseError({});
assert.isString(e5);
assert.isEmpty(e5);
});
it('getAuthoritativeDnsResolver()', async () => {
/* valid domain - should not use global default */
const r1 = await util.getAuthoritativeDnsResolver('example.com');
assert.instanceOf(r1, dns.Resolver);
assert.isNotEmpty(r1.getServers());
assert.notDeepEqual(r1.getServers(), dns.getServers());
/* invalid domain - fallback to global default */
const r2 = await util.getAuthoritativeDnsResolver('invalid.xtldx');
assert.instanceOf(r2, dns.Resolver);
assert.deepStrictEqual(r2.getServers(), dns.getServers());
});
/* TODO: Figure out how to test this */
it('retrieveTlsAlpnCertificate()');
});

View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDzzCCA1WgAwIBAgISA0ghDoSv5DpT3Pd3lqwjbVDDMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
NjAeFw0yNDA2MTAxNzEyMjZaFw0yNDA5MDgxNzEyMjVaMBQxEjAQBgNVBAMTCWxl
bmNyLm9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHJ3DjN7pYV3mftHzaP
V/WI0RhOJnSI5AIFEPFHDi8UowOINRGIfm9FHGIDqrb4Rmyvr9JrrqBdFGDen8BW
6OGjggJnMIICYzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIdCTnxqmpOELDyzPaEM
seB36lUOMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF
BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG
AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMG8GA1UdEQRoMGaCCWxlbmNy
Lm9yZ4IPbGV0c2VuY3J5cHQuY29tgg9sZXRzZW5jcnlwdC5vcmeCDXd3dy5sZW5j
ci5vcmeCE3d3dy5sZXRzZW5jcnlwdC5jb22CE3d3dy5sZXRzZW5jcnlwdC5vcmcw
EwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEAdgA/
F0tP1yJHWJQdZRyEvg0S7ZA3fx+FauvBvyiF7PhkbgAAAZADWfneAAAEAwBHMEUC
IGlp+dPU2hLT2suTMYkYMlt/xbzSnKLZDA/wYSsPACP7AiEAxbAzx6mkzn0cs0hh
ti6sLf0pcbmDhxHdlJRjuo6SQZEAdwDf4VbrqgWvtZwPhnGNqMAyTq5W2W6n9aVq
AdHBO75SXAAAAZADWfqrAAAEAwBIMEYCIQCrAmDUrlX3oGhri1qCIb65Cuf8h2GR
LC1VfXBenX7dCAIhALXwbhCQ1vO1WLv4CqyihMHOwFaICYqN/N6ylaBlVAM4MAoG
CCqGSM49BAMDA2gAMGUCMFdgjOXGl+hE2ABDsAeuNq8wi34yTMUHk0KMTOjRAfy9
rOCGQqvP0myoYlyzXOH9uQIxAMdkG1ZWBZS1dHavbPf1I/MjYpzX6gy0jVHIXXu5
aYWylBi/Uf2RPj0LWFZh8tNa1Q==
-----END CERTIFICATE-----

View File

@@ -29,6 +29,13 @@ if (process.env.ACME_TLSALPN_PORT) {
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
}
/**
* Greatly reduce retry duration while testing
*/
axios.defaults.acmeSettings.retryMaxAttempts = 3;
axios.defaults.acmeSettings.retryDefaultDelay = 1;
/**
* External account binding
*/

View File

@@ -45,6 +45,7 @@ export interface ClientOptions {
backoffMin?: number;
backoffMax?: number;
urlMapping?: UrlMapping;
signal?: AbortSignal;
}
export interface ClientExternalAccountBindingOptions {
@@ -61,6 +62,7 @@ export interface ClientAutoOptions {
skipChallengeVerification?: boolean;
challengePriority?: string[];
preferredChain?: string;
signal?: AbortSignal;
}
export class Client {

View File

@@ -24,3 +24,5 @@ dist-ssr
*.sw?
test/user.secret.*
test/**/*.js
src/**/*.spec.ts

View File

@@ -1,5 +1,4 @@
{
"extension": ["ts"],
"spec": "test/**/*.test.ts",
"require": "ts-node/register"
}
"spec": "src/**/*.spec.ts"
}

View File

@@ -1,2 +1,3 @@
node_modules
src
src
dist/**/*.spec.*

View File

@@ -3,6 +3,52 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes
* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7))
* 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5))
### Features
* 支持google证书申请需要使用代理 ([a593056](https://github.com/certd/certd/commit/a593056e79e99dd6a74f75b5eab621af7248cfbe))
### Performance Improvements
* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b))
* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345))
* email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f))
## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
**Note:** Version bump only for package @certd/pipeline
## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05)
### Performance Improvements
* 优化pipeline删除时删除其他history ([b425203](https://github.com/certd/certd/commit/b4252033d56a9ad950f3e204ff021497c3978015))
## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04)
**Note:** Version bump only for package @certd/pipeline
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
### Bug Fixes
* 修复在相同的cron时偶尔无法触发定时任务的bug ([680941a](https://github.com/certd/certd/commit/680941af119619006b592e3ab6fb112cb5556a8b))
### Performance Improvements
* 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7))
* 优化前置任务输出为空的提示 ([6ed1e18](https://github.com/certd/certd/commit/6ed1e18c7d9c46d964ecc6abc90f3908297b7632))
## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26)
**Note:** Version bump only for package @certd/pipeline
## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25)
**Note:** Version bump only for package @certd/pipeline

View File

@@ -0,0 +1 @@
14:26

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/pipeline",
"private": false,
"version": "1.22.3",
"version": "1.24.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -10,10 +10,10 @@
"build": "tsc --skipLibCheck",
"build3": "rollup -c",
"build2": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "mocha --loader=ts-node/esm"
},
"dependencies": {
"@types/lodash-es": "^4.17.12",
"axios": "^1.7.2",
"fix-path": "^4.0.0",
"lodash-es": "^4.17.21",
@@ -28,6 +28,7 @@
"@rollup/plugin-terser": "^0.4.3",
"@rollup/plugin-typescript": "^11.0.0",
"@types/chai": "^4.3.10",
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.1",
"@types/node-forge": "^1.3.2",
"@types/uuid": "^9.0.2",

View File

@@ -23,16 +23,19 @@ export type ExecutorOptions = {
emailService: IEmailService;
fileRootDir?: string;
};
export class Executor {
pipeline: Pipeline;
runtime!: RunHistory;
contextFactory: ContextFactory;
logger: Logger;
pipelineContext!: IContext;
currentStatusMap!: RunnableCollection;
lastStatusMap!: RunnableCollection;
lastRuntime!: RunHistory;
options: ExecutorOptions;
canceled = false;
abort: AbortController = new AbortController();
onChanged: (history: RunHistory) => Promise<void>;
constructor(options: ExecutorOptions) {
this.options = options;
@@ -50,10 +53,11 @@ export class Executor {
const lastRuntime = await this.pipelineContext.getObj(`lastRuntime`);
this.lastRuntime = lastRuntime;
this.lastStatusMap = new RunnableCollection(lastRuntime?.pipeline);
this.currentStatusMap = new RunnableCollection(this.pipeline);
}
async cancel() {
this.canceled = true;
this.abort.abort();
this.runtime?.cancel(this.pipeline);
await this.onChanged(this.runtime);
}
@@ -102,6 +106,8 @@ export class Executor {
}
}
if (lastResult != null && lastResult === ResultType.success && !inputChanged) {
runnable.status!.output = lastNode?.status?.output;
runnable.status!.files = lastNode?.status?.files;
this.runtime.skip(runnable);
await this.onChanged(this.runtime);
return ResultType.skip;
@@ -113,10 +119,15 @@ export class Executor {
// const timeout = runnable.timeout ?? 20 * 60 * 1000;
try {
if (this.canceled) {
if (this.abort.signal.aborted) {
this.runtime.cancel(runnable);
return ResultType.canceled;
}
await run();
if (this.abort.signal.aborted) {
this.runtime.cancel(runnable);
return ResultType.canceled;
}
this.runtime.success(runnable);
return ResultType.success;
} catch (e: any) {
@@ -197,7 +208,7 @@ export class Executor {
private async runStep(step: Step) {
const currentLogger = this.runtime._loggers[step.id];
this.currentStatusMap.add(step);
const lastStatus = this.lastStatusMap.get(step.id);
//执行任务
const plugin: RegistryItem<AbstractTaskPlugin> = pluginRegistry.get(step.type);
@@ -211,11 +222,11 @@ export class Executor {
if (item.component?.name === "pi-output-selector") {
const contextKey = step.input[key];
if (contextKey != null) {
const value = this.runtime.context[contextKey];
if (value == null) {
currentLogger.warn(`[step init] input ${define.title} is null`);
}
step.input[key] = value;
// "cert": "step.-BNFVPMKPu2O-i9NiOQxP.cert",
const arr = contextKey.split(".");
const id = arr[1];
const outputKey = arr[2];
step.input[key] = this.currentStatusMap.get(id)?.status?.output[outputKey] ?? this.lastStatusMap.get(id)?.status?.output[outputKey];
}
}
});
@@ -236,23 +247,32 @@ export class Executor {
parent: this.runtime.id,
rootDir: this.options.fileRootDir,
}),
signal: this.abort.signal,
};
instance.setCtx(taskCtx);
await instance.onInstance();
await instance.execute();
//执行结果处理
if (instance._result.clearLastStatus) {
//是否需要清除所有状态
this.lastStatusMap.clear();
}
//输出到output context
//输出上下文变量到output context
_.forEach(define.output, (item: any, key: any) => {
step.status!.output[key] = instance[key];
const stepOutputKey = `step.${step.id}.${key}`;
this.runtime.context[stepOutputKey] = instance[key];
// const stepOutputKey = `step.${step.id}.${key}`;
// this.runtime.context[stepOutputKey] = instance[key];
});
step.status!.files = instance.getFiles();
//更新pipeline vars
if (Object.keys(instance._result.pipelineVars).length > 0) {
// 判断 pipelineVars 有值时更新
const vars = this.pipelineContext.getObj("vars");
_.merge(vars, instance._result.pipelineVars);
await this.pipelineContext.setObj("vars", vars);
}
}
async notification(when: NotificationWhen, error?: any) {

View File

@@ -18,7 +18,9 @@ export interface IFileStore {
export class FileStore {
rootDir: string;
// pipelineId
scope: string;
// historyId
parent: string;
constructor(options?: FileStoreOptions) {
this.rootDir = fileUtils.getFileRootDir(options?.rootDir);
@@ -52,7 +54,10 @@ export class FileStore {
deleteByParent(scope: string, parent: string) {
const dir = path.join(this.rootDir, scope, parent);
if (fs.existsSync(dir)) {
fs.unlinkSync(dir);
fs.rmSync(dir, {
recursive: true,
force: true,
});
}
}
}

View File

@@ -3,3 +3,4 @@ export * from "./run-history.js";
export * from "./context.js";
export * from "./storage.js";
export * from "./file-store.js";
export * from "./license.js";

View File

@@ -0,0 +1,14 @@
import { isPlus, verify } from "./license.js";
import { equal } from "assert";
describe("license", function () {
it("#license", async function () {
const req = {
subjectId: "999",
license: "",
};
const plus = isPlus();
equal(plus, false);
const res = await verify(req);
equal(res, true);
});
});

View File

@@ -0,0 +1,138 @@
import { createVerify } from "node:crypto";
import { logger } from "../utils/index.js";
import dayjs from "dayjs";
let SecreteKey =
"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQW9VWE1EWUhjdi82WFROWEZFSUI2RlpuR2FER0cwZnR5bTV1dVhPck9NaVl0UkxSb1lvSGMKNVZxenE0N00rdEFqRFBhaTBlOFhWS1c3aytUQUw3MUs0N2JCQVEyWTBxNU5Ya3lYcE5PTVdueVFMYXBwb0tWNgpPMkFJMnpFVURWMVJVa0ZtMFZTVjU0VXNzMDcrdjI2aW5aQU1CWitDMU42eWFDc2tZL3grNnVlNkVRNVcyZXdFCjZOWEhJcUU1bHdEUmU2SXJtdEpnU2doSnlHTS91azIyejN6NGEraFVPVUlWMy9DbEhYV0VhRHBBRFFsakt3NSsKeHR0dURiTHZyUmdzdWp6czB0dEI2OE1SbXE0R0FJL0JtNWVPWkhlNGxFQjBFVVhFUXdVWE1jV1N1VFZSMUE2cApUM21LRGo5MGcwVDFZUlNOdE5TMm9aRzgvRWIwOVlxK3Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
let appKey = "kQth6FHM71IPV3qdWc";
if (process.env.NODE_ENV !== "production") {
SecreteKey =
"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXY3TGtMaUp1dGM0NzhTU3RaTExjajVGZXh1YjJwR2NLMGxwa0hwVnlZWjhMY29rRFhuUlAKUGQ5UlJSTVRTaGJsbFl2Mzd4QUhOV1ZIQ0ZsWHkrQklVU001bUlBU1NDQTV0azlJNmpZZ2F4bEFDQm1BY0lGMwozKzBjeGZIYVkrVW9YdVluMkZ6YUt2Ym5GdFZIZ0lkMDg4a3d4clZTZzlCT3BDRVZIR1pxR2I5TWN5MXVHVXhUClFTVENCbmpoTWZlZ0p6cXVPYWVOY0ZPSE5tbmtWRWpLTythbTBPeEhNS1lyS3ZnQnVEbzdoVnFENlBFMUd6V3AKZHdwZUV4QXZDSVJxL2pWTkdRK3FtMkRWOVNJZ3U5bmF4MktmSUtFeU50dUFFS1VpekdqL0VmRFhDM1cxMExhegpKaGNYNGw1SUFZU1o3L3JWVmpGbExWSVl0WDU1T054L1Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
appKey = "z4nXOeTeSnnpUpnmsV1";
}
export const AppKey = appKey;
export type LicenseVerifyReq = {
subjectId: string;
license: string;
};
type License = {
appKey: string;
code: string;
subjectId: string;
expireTime: number;
activeTime: number;
duration: number;
version: number;
secret: string;
vipType: string;
signature: string;
};
class LicenseHolder {
isPlus = false;
expireTime = 0;
vipType = "";
message?: string = undefined;
secret?: string = undefined;
}
const holder = new LicenseHolder();
holder.isPlus = false;
class LicenseVerifier {
checked = false;
licenseReq?: LicenseVerifyReq = undefined;
async reVerify(req: LicenseVerifyReq) {
this.checked = false;
return await this.verify(req);
}
setPlus(value: boolean, info: any = {}) {
if (value && info) {
holder.isPlus = true;
holder.expireTime = info.expireTime;
holder.secret = info.secret;
holder.vipType = info.vipType;
} else {
holder.isPlus = false;
holder.expireTime = 0;
holder.vipType = "";
holder.message = info.message;
holder.secret = undefined;
}
return {
...holder,
};
}
async verify(req: LicenseVerifyReq) {
this.licenseReq = req;
if (this.checked) {
return this.setPlus(false);
}
const license = req?.license;
if (!license) {
this.checked = true;
return this.setPlus(false);
}
const licenseJson = Buffer.from(license, "base64").toString();
const json: License = JSON.parse(licenseJson);
if (json.expireTime < Date.now()) {
logger.warn("授权已过期");
return this.setPlus(false, { message: "授权已过期" });
}
const content = `${appKey},${this.licenseReq.subjectId},${json.code},${json.secret},${json.vipType},${json.activeTime},${json.duration},${json.expireTime},${json.version}`;
// content := fmt.Sprintf("%s,%s,%s,%s,%d,%d,%d,%d,%d", entity.AppKey, entity.SubjectId, entity.Code, entity.Secret, entity.Level, entity.ActiveTime, entity.Duration, entity.ExpireTime, entity.Version)
//z4nXOeTeSnnpUpnmsV,_m9jFTdNHktdaEN4xBDw_,HZz7rAAR3h3zGlDMhScO1wGBYPjXpZ9S_1,uUpr9I8p6K3jWSzu2Wh5NECvgG2FNynU,0,1724199847470,365,1787271324416,1
logger.debug("content:", content);
const publicKey = Buffer.from(SecreteKey, "base64").toString();
const res = this.verifySignature(content, json.signature, publicKey);
this.checked = true;
if (!res) {
logger.warn("授权校验失败");
return this.setPlus(false, { message: "授权校验失败" });
}
logger.info(`授权校验成功,到期时间:${dayjs(json.expireTime).format("YYYY-MM-DD HH:mm:ss")}`);
return this.setPlus(true, {
expireTime: json.expireTime,
vipType: json.vipType || "plus",
secret: json.secret,
});
}
verifySignature(content: string, signature: any, publicKey: string) {
const verify = createVerify("RSA-SHA256");
verify.update(content);
return verify.verify(publicKey, signature, "base64");
}
}
const verifier = new LicenseVerifier();
export function isPlus() {
return holder.isPlus && holder.expireTime > Date.now();
}
export function isCommercial() {
return holder.isPlus && holder.vipType === "comm" && holder.expireTime > Date.now();
}
export function getPlusInfo() {
return {
isPlus: holder.isPlus,
vipType: holder.vipType,
expireTime: holder.expireTime,
secret: holder.secret,
};
}
export async function verify(req: LicenseVerifyReq) {
try {
return await verifier.reVerify(req);
} catch (e) {
logger.error(e);
return verifier.setPlus(false, { message: "授权校验失败" });
}
}

View File

@@ -1,4 +1,4 @@
import { Context, HistoryResult, Pipeline, ResultType, Runnable, RunnableMap, Stage, Step, Task } from "../dt/index.js";
import { HistoryResult, Pipeline, ResultType, Runnable, RunnableMap, Stage, Step, Task } from "../dt/index.js";
import _ from "lodash-es";
import { buildLogger } from "../utils/util.log.js";
import { Logger } from "log4js";
@@ -14,15 +14,12 @@ export type RunTrigger = {
export function NewRunHistory(obj: any) {
const history = new RunHistory(obj.id, obj.trigger, obj.pipeline);
history.context = obj.context;
history.logs = obj.logs;
history._loggers = obj.loggers;
return history;
}
export class RunHistory {
id!: string;
//运行时上下文变量
context: Context = {};
pipeline!: Pipeline;
logs: {
[runnableId: string]: string[];
@@ -168,4 +165,8 @@ export class RunnableCollection {
item.status = undefined;
});
}
add(runnable: Runnable) {
this.collection[runnable.id] = runnable;
}
}

View File

@@ -6,7 +6,7 @@ import { IAccessService } from "../access/index.js";
import { IEmailService } from "../service/index.js";
import { IContext } from "../core/index.js";
import { AxiosInstance } from "axios";
import { logger } from "../utils/index.js";
import { ILogger, logger } from "../utils/index.js";
export enum ContextScope {
global,
@@ -51,6 +51,7 @@ export type ITaskPlugin = {
export type TaskResult = {
clearLastStatus?: boolean;
files?: FileItem[];
pipelineVars: Record<string, any>;
};
export type TaskInstanceContext = {
pipeline: Pipeline;
@@ -63,11 +64,15 @@ export type TaskInstanceContext = {
http: AxiosInstance;
fileStore: FileStore;
lastStatus?: Runnable;
signal: AbortSignal;
};
export abstract class AbstractTaskPlugin implements ITaskPlugin {
_result: TaskResult = { clearLastStatus: false, files: [] };
_result: TaskResult = { clearLastStatus: false, files: [], pipelineVars: {} };
ctx!: TaskInstanceContext;
logger!: ILogger;
accessService!: IAccessService;
clearLastStatus() {
this._result.clearLastStatus = true;
}
@@ -78,17 +83,13 @@ export abstract class AbstractTaskPlugin implements ITaskPlugin {
setCtx(ctx: TaskInstanceContext) {
this.ctx = ctx;
this.logger = ctx.logger;
this.accessService = ctx.accessService;
}
randomFileId() {
return Math.random().toString(36).substring(2, 9);
}
linkFile(file: FileItem) {
this._result.files?.push({
...file,
id: this.randomFileId(),
});
}
saveFile(filename: string, file: Buffer) {
const filePath = this.ctx.fileStore.writeFile(filename, file);
logger.info(`saveFile:${filePath}`);

View File

@@ -53,7 +53,22 @@ export function createAxiosService({ logger }: { logger: Logger }) {
logger.error(`请求出错url:${error?.response?.config.url},method:${error?.response?.config?.method},status:${error?.response?.status}`);
logger.info("返回数据:", JSON.stringify(error?.response?.data));
delete error.config;
delete error.response;
const data = error?.response?.data;
if (!data) {
error.message = data.message || data.msg || data.error || data;
}
if (error?.response) {
return Promise.reject({
status: error?.response?.status,
statusText: error?.response?.statusText,
request: {
url: error?.response?.config?.url,
method: error?.response?.config?.method,
data: error?.response?.data,
},
data: error?.response?.data,
});
}
return Promise.reject(error);
}
);

View File

@@ -5,4 +5,3 @@ export default function (timeout: number) {
}, timeout);
});
}

View File

@@ -72,7 +72,7 @@ async function spawn(opts: SpawnOption): Promise<string> {
let stderr = "";
return safePromise((resolve, reject) => {
const ls = childProcess.spawn(cmd, {
shell: process.platform == "win32",
shell: true,
env: {
...process.env,
...opts.env,

View File

@@ -1,6 +1,6 @@
import { expect } from "chai";
import "mocha";
import { EchoPlugin } from "./echo-plugin";
import { EchoPlugin } from "./echo-plugin.js";
describe("task_plugin", function () {
it("#taskplugin", function () {
console.log("before new plugin");

View File

@@ -0,0 +1 @@
license.*

View File

@@ -34,6 +34,7 @@
"exclude": [
"*.js",
"*.ts",
"*.spec.ts",
"dist",
"node_modules",
"test"

View File

@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Performance Improvements
* 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd))
## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
**Note:** Version bump only for package @certd/lib-k8s
## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05)
**Note:** Version bump only for package @certd/lib-k8s
## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04)
**Note:** Version bump only for package @certd/lib-k8s
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
**Note:** Version bump only for package @certd/lib-k8s
## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26)
**Note:** Version bump only for package @certd/lib-k8s
## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25)
**Note:** Version bump only for package @certd/lib-k8s

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/lib-k8s",
"private": false,
"version": "1.22.3",
"version": "1.24.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,11 +13,10 @@
"preview": "vite preview"
},
"dependencies": {
"kubernetes-client": "^9.0.0",
"shelljs": "^0.8.5"
"@kubernetes/client-node": "0.21.0"
},
"devDependencies": {
"@certd/pipeline": "^1.22.3",
"@certd/pipeline": "^1.24.0",
"@rollup/plugin-commonjs": "^23.0.4",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",

View File

@@ -1,30 +1,40 @@
import kubernetesClient from 'kubernetes-client';
//@ts-ignore
import { KubeConfig, CoreV1Api, V1Secret, NetworkingV1Api, V1Ingress } from '@kubernetes/client-node';
import dns from 'dns';
import { logger } from '@certd/pipeline';
import { ILogger } from '@certd/pipeline';
//@ts-ignore
const { KubeConfig, Client, Request } = kubernetesClient;
export class K8sClient {
export type K8sClientOpts = {
kubeConfigStr: string;
lookup!: any;
client!: any;
constructor(kubeConfigStr: string) {
this.kubeConfigStr = kubeConfigStr;
logger: ILogger;
//{ [domain]:{ip:'xxx.xx.xxx'} }
//暂时没用
lookup?: any;
};
export class K8sClient {
kubeconfig!: KubeConfig;
kubeConfigStr: string;
lookup!: (hostnameReq: any, options: any, callback: any) => void;
client!: CoreV1Api;
logger: ILogger;
constructor(opts: K8sClientOpts) {
this.kubeConfigStr = opts.kubeConfigStr;
this.logger = opts.logger;
this.setLookup(opts.lookup);
this.init();
}
init() {
const kubeconfig = new KubeConfig();
kubeconfig.loadFromString(this.kubeConfigStr);
const reqOpts = { kubeconfig, request: {} } as any;
if (this.lookup) {
reqOpts.request.lookup = this.lookup;
}
this.kubeconfig = kubeconfig;
this.client = kubeconfig.makeApiClient(CoreV1Api);
const backend = new Request(reqOpts);
this.client = new Client({ backend, version: '1.13' });
// const reqOpts = { kubeconfig, request: {} } as any;
// if (this.lookup) {
// reqOpts.request.lookup = this.lookup;
// }
//
// const backend = new Request(reqOpts);
// this.client = new Client({ backend, version: '1.13' });
}
/**
@@ -32,16 +42,18 @@ export class K8sClient {
* @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
*/
setLookup(localRecords: { [key: string]: { ip: string } }) {
if (localRecords == null) {
return;
}
this.lookup = (hostnameReq: any, options: any, callback: any) => {
logger.info('custom lookup', hostnameReq, localRecords);
this.logger.info('custom lookup', hostnameReq, localRecords);
if (localRecords[hostnameReq]) {
logger.info('local record', hostnameReq, localRecords[hostnameReq]);
this.logger.info('local record', hostnameReq, localRecords[hostnameReq]);
callback(null, localRecords[hostnameReq].ip, 4);
} else {
dns.lookup(hostnameReq, options, callback);
}
};
this.init();
}
/**
@@ -49,9 +61,9 @@ export class K8sClient {
* @param opts = {namespace:default}
* @returns secretsList
*/
async getSecret(opts: { namespace: string }) {
async getSecrets(opts: { namespace: string }) {
const namespace = opts.namespace || 'default';
return await this.client.api.v1.namespaces(namespace).secrets.get();
return await this.client.listNamespacedSecret(namespace);
}
/**
@@ -59,59 +71,61 @@ export class K8sClient {
* @param opts {namespace:default, body:yamlStr}
* @returns {Promise<*>}
*/
async createSecret(opts: any) {
async createSecret(opts: { namespace: string; body: V1Secret }) {
const namespace = opts.namespace || 'default';
const created = await this.client.api.v1.namespaces(namespace).secrets.post({
body: opts.body,
});
logger.info('new secrets:', created);
return created;
const created = await this.client.createNamespacedSecret(namespace, opts.body);
this.logger.info('new secrets:', created.body);
return created.body;
}
async updateSecret(opts: any) {
// async updateSecret(opts: any) {
// const namespace = opts.namespace || 'default';
// const secretName = opts.secretName;
// if (secretName == null) {
// throw new Error('secretName 不能为空');
// }
// return await this.client.replaceNamespacedSecret(secretName, namespace, opts.body);
// }
async patchSecret(opts: { namespace: string; secretName: string; body: V1Secret }) {
const namespace = opts.namespace || 'default';
const secretName = opts.secretName;
if (secretName == null) {
throw new Error('secretName 不能为空');
}
return await this.client.api.v1.namespaces(namespace).secrets(secretName).put({
body: opts.body,
});
const res = await this.client.patchNamespacedSecret(secretName, namespace, opts.body);
this.logger.info('secret patched:', res.body);
return res.body;
}
async patchSecret(opts: any) {
async getIngressList(opts: { namespace: string }) {
const namespace = opts.namespace || 'default';
const secretName = opts.secretName;
if (secretName == null) {
throw new Error('secretName 不能为空');
}
return await this.client.api.v1.namespaces(namespace).secrets(secretName).patch({
body: opts.body,
});
const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
const res = await client.listNamespacedIngress(namespace);
this.logger.info('ingress list get:', res.body);
return res.body;
}
async getIngressList(opts: any) {
const namespace = opts.namespace || 'default';
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses.get();
}
// async getIngress(opts: { namespace: string; ingressName: string }) {
// const namespace = opts.namespace || 'default';
// const ingressName = opts.ingressName;
// if (!ingressName) {
// throw new Error('ingressName 不能为空');
// }
// const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
// const res = await client.listNamespacedIngress();
// return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get();
// }
async getIngress(opts: any) {
async patchIngress(opts: { namespace: string; ingressName: string; body: V1Ingress }) {
const namespace = opts.namespace || 'default';
const ingressName = opts.ingressName;
if (!ingressName) {
throw new Error('ingressName 不能为空');
}
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).get();
}
async patchIngress(opts: any) {
const namespace = opts.namespace || 'default';
const ingressName = opts.ingressName;
if (!ingressName) {
throw new Error('ingressName 不能为空');
}
return await this.client.apis.extensions.v1beta1.namespaces(namespace).ingresses(ingressName).patch({
body: opts.body,
});
const client = this.kubeconfig.makeApiClient(NetworkingV1Api);
const res = await client.patchNamespacedIngress(ingressName, namespace, opts.body);
this.logger.info('ingress patched:', res.body);
return res;
}
}

View File

@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
**Note:** Version bump only for package @certd/midway-flyway-js
## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25)
**Note:** Version bump only for package @certd/midway-flyway-js

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/midway-flyway-js",
"version": "1.22.3",
"version": "1.22.6",
"description": "midway with flyway, sql upgrade way ",
"private": false,
"type": "module",

View File

@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes
* 修复成功后跳过之后丢失腾讯云证书id的bug ([37eb762](https://github.com/certd/certd/commit/37eb762afe25c5896b75dee25f32809f8426e7b7))
* 修复创建流水线后立即运行时报no id错误的bug ([17ead54](https://github.com/certd/certd/commit/17ead547aab25333603980304aa3aad3db1f73d5))
* 修复使用代理的情况下申请证书失败的bug ([95122e2](https://github.com/certd/certd/commit/95122e28609333f4df55c266e5434897954c0fb3))
### Features
* 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473))
### Performance Improvements
* 更新k8s底层api库 ([746bb9d](https://github.com/certd/certd/commit/746bb9d385e2f397daef4976eca1d4782a2f5ebd))
* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b))
* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345))
## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Performance Improvements
* 优化插件字段的default value ([24c7be2](https://github.com/certd/certd/commit/24c7be2c9cb39c14f7a97b674127c88033280b02))
## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05)
**Note:** Version bump only for package @certd/plugin-cert
## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04)
**Note:** Version bump only for package @certd/plugin-cert
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
### Performance Improvements
* 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7))
## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26)
**Note:** Version bump only for package @certd/plugin-cert
## [1.22.4](https://github.com/certd/certd/compare/v1.22.3...v1.22.4) (2024-07-26)
### Performance Improvements

View File

@@ -1,7 +1,7 @@
{
"name": "@certd/plugin-cert",
"private": false,
"version": "1.22.4",
"version": "1.24.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
@@ -13,8 +13,8 @@
"preview": "vite preview"
},
"dependencies": {
"@certd/acme-client": "^1.22.4",
"@certd/pipeline": "^1.22.3",
"@certd/acme-client": "^1.24.0",
"@certd/pipeline": "^1.24.0",
"jszip": "^3.10.1",
"node-forge": "^0.10.0",
"psl": "^1.9.0"

View File

@@ -13,7 +13,8 @@ export type CertInfo = {
key: string;
csr: string;
};
export type SSLProvider = "letsencrypt" | "buypass" | "zerossl";
export type SSLProvider = "letsencrypt" | "google" | "zerossl";
export type PrivateKeyType = "rsa_1024" | "rsa_2048" | "rsa_3072" | "rsa_4096" | "ec_256" | "ec_384" | "ec_521";
type AcmeServiceOptions = {
userContext: IContext;
logger: Logger;
@@ -21,6 +22,8 @@ type AcmeServiceOptions = {
eab?: ClientExternalAccountBindingOptions;
skipLocalVerify?: boolean;
useMappingProxy?: boolean;
privateKeyType?: PrivateKeyType;
signal?: AbortSignal;
};
export class AcmeService {
@@ -42,8 +45,20 @@ export class AcmeService {
});
}
async getAccountConfig(email: string): Promise<any> {
return (await this.userContext.getObj(this.buildAccountKey(email))) || {};
async getAccountConfig(email: string, urlMapping: UrlMapping): Promise<any> {
const conf = (await this.userContext.getObj(this.buildAccountKey(email))) || {};
if (urlMapping && urlMapping.mappings) {
for (const key in urlMapping.mappings) {
if (Object.prototype.hasOwnProperty.call(urlMapping.mappings, key)) {
const element = urlMapping.mappings[key];
if (conf.accountUrl?.indexOf(element) > -1) {
//如果用了代理url要替换回去
conf.accountUrl = conf.accountUrl.replace(element, key);
}
}
}
}
return conf;
}
buildAccountKey(email: string) {
@@ -55,7 +70,14 @@ export class AcmeService {
}
async getAcmeClient(email: string, isTest = false): Promise<acme.Client> {
const conf = await this.getAccountConfig(email);
const urlMapping: UrlMapping = {
enabled: false,
mappings: {
"acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work",
"dv.acme-v02.api.pki.goog": "google.proxy.handsfree.work",
},
};
const conf = await this.getAccountConfig(email, urlMapping);
if (conf.key == null) {
conf.key = await this.createNewKey();
await this.saveAccountConfig(email, conf);
@@ -66,22 +88,19 @@ export class AcmeService {
} else {
directoryUrl = acme.directory[this.sslProvider].production;
}
const urlMapping: UrlMapping = { enabled: false, mappings: {} };
if (this.options.useMappingProxy) {
urlMapping.enabled = true;
urlMapping.mappings = {
"acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work",
};
}
const client = new acme.Client({
directoryUrl: directoryUrl,
accountKey: conf.key,
accountUrl: conf.accountUrl,
externalAccountBinding: this.eab,
backoffAttempts: 30,
backoffAttempts: 15,
backoffMin: 5000,
backoffMax: 10000,
urlMapping,
signal: this.options.signal,
});
if (conf.accountUrl == null) {
@@ -193,18 +212,38 @@ export class AcmeService {
}
}
async order(options: { email: string; domains: string | string[]; dnsProvider: any; csrInfo: any; isTest?: boolean }) {
async order(options: {
email: string;
domains: string | string[];
dnsProvider: any;
csrInfo: any;
isTest?: boolean;
privateKeyType?: string;
}): Promise<CertInfo> {
const { email, isTest, domains, csrInfo, dnsProvider } = options;
const client: acme.Client = await this.getAcmeClient(email, isTest);
/* Create CSR */
const { commonName, altNames } = this.buildCommonNameByDomains(domains);
const [key, csr] = await acme.forge.createCsr({
commonName,
...csrInfo,
altNames,
});
let privateKey = null;
const privateKeyType = options.privateKeyType || "rsa_2048";
const privateKeyArr = privateKeyType.split("_");
const type = privateKeyArr[0];
const size = parseInt(privateKeyArr[1]);
if (type == "ec") {
const name: any = "P-" + size;
privateKey = await acme.crypto.createPrivateEcdsaKey(name);
} else {
privateKey = await acme.crypto.createPrivateRsaKey(size);
}
const [key, csr] = await acme.crypto.createCsr(
{
commonName,
...csrInfo,
altNames,
},
privateKey
);
if (dnsProvider == null) {
throw new Error("dnsProvider 不能为空");
}
@@ -221,6 +260,7 @@ export class AcmeService {
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
},
signal: this.options.signal,
});
const cert: CertInfo = {

View File

@@ -1,7 +1,6 @@
import { AbstractTaskPlugin, HttpClient, IAccessService, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import { AbstractTaskPlugin, HttpClient, IContext, Step, TaskInput, TaskOutput } from "@certd/pipeline";
import dayjs from "dayjs";
import type { CertInfo } from "./acme.js";
import { Logger } from "log4js";
import { CertReader } from "./cert-reader.js";
import JSZip from "jszip";
@@ -91,9 +90,7 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
// })
csrInfo!: string;
logger!: Logger;
userContext!: IContext;
accessService!: IAccessService;
http!: HttpClient;
lastStatus!: Step;
@@ -103,8 +100,6 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
cert?: CertInfo;
async onInstance() {
this.accessService = this.ctx.accessService;
this.logger = this.ctx.logger;
this.userContext = this.ctx.userContext;
this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step;
@@ -138,8 +133,10 @@ export abstract class CertApplyBasePlugin extends AbstractTaskPlugin {
const cert: CertInfo = certReader.toCertInfo();
this.cert = cert;
this._result.pipelineVars.certExpiresTime = dayjs(certReader.detail.notAfter).valueOf();
if (isNew) {
const applyTime = dayjs(certReader.detail.validity.notBefore).format("YYYYMMDD_HHmmss");
const applyTime = dayjs(certReader.detail.notBefore).format("YYYYMMDD_HHmmss");
await this.zipCert(cert, applyTime);
} else {
this.extendsFiles();

View File

@@ -1,8 +1,8 @@
import { CertInfo } from "./acme.js";
import fs from "fs";
import os from "os";
import forge from "node-forge";
import path from "path";
import { crypto } from "@certd/acme-client";
export class CertReader implements CertInfo {
crt: string;
key: string;
@@ -29,9 +29,8 @@ export class CertReader implements CertInfo {
}
getCrtDetail(crt: string) {
const pki = forge.pki;
const detail = pki.certificateFromPem(crt.toString());
const expires = detail.validity.notAfter;
const detail = crypto.readCertificateInfo(crt.toString());
const expires = detail.notAfter;
return { detail, expires };
}

View File

@@ -1,5 +1,5 @@
import { Decorator, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline";
import type { CertInfo, SSLProvider } from "./acme.js";
import type { CertInfo, PrivateKeyType, SSLProvider } from "./acme.js";
import { AcmeService } from "./acme.js";
import _ from "lodash-es";
import { DnsProviderContext, DnsProviderDefine, dnsProviderRegistry } from "../../dns-provider/index.js";
@@ -27,20 +27,40 @@ export type { CertInfo };
export class CertApplyPlugin extends CertApplyBasePlugin {
@TaskInput({
title: "证书提供商",
default: "letsencrypt",
value: "letsencrypt",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "letsencrypt", label: "Let's Encrypt" },
// { value: "letsencrypt-proxy", label: "Let's Encrypt代理letsencrypt.org无法访问时使用" },
// { value: "buypass", label: "Buypass" },
{ value: "google", label: "Google" },
{ value: "zerossl", label: "ZeroSSL" },
],
},
helper: "如果letsencrypt.org或dv.acme-v02.api.pki.goog无法访问请尝试开启代理选项\n如果使用ZeroSSL、google证书需要提供EAB授权",
required: true,
})
sslProvider!: SSLProvider;
@TaskInput({
title: "加密算法",
value: "rsa_2048",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: "rsa_1024", label: "RSA 1024" },
{ value: "rsa_2048", label: "RSA 2048" },
{ value: "rsa_3072", label: "RSA 3072" },
{ value: "rsa_4096", label: "RSA 4096" },
{ value: "ec_256", label: "EC 256" },
{ value: "ec_384", label: "EC 384" },
// { value: "ec_521", label: "EC 521" },
],
},
required: true,
})
sslProvider!: SSLProvider;
privateKeyType!: PrivateKeyType;
@TaskInput({
title: "EAB授权",
@@ -49,7 +69,8 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
type: "eab",
},
maybeNeed: true,
helper: "如果使用ZeroSSL证书需要提供EAB授权 请前往 https://app.zerossl.com/developer 生成 'EAB Credentials for ACME Clients' ",
helper:
"如果使用ZeroSSL或者google证书需要提供EAB授权\nZeroSSL请前往 https://app.zerossl.com/developer 生成 'EAB Credentials' \n Google请前往https://github.com/certd/certd/blob/v2/doc/google/google.md",
})
eabAccessId!: number;
@@ -82,18 +103,19 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
@TaskInput({
title: "使用代理",
default: false,
value: false,
component: {
name: "a-switch",
vModel: "checked",
},
helper: "如果acme-v02.api.letsencrypt.org被墙无法连接访问请尝试开启此选项",
maybeNeed: true,
helper: "如果acme-v02.api.letsencrypt.org或dv.acme-v02.api.pki.goog被墙无法访问请尝试开启此选项",
})
useProxy = false;
@TaskInput({
title: "跳过本地校验DNS",
default: false,
value: false,
component: {
name: "a-switch",
vModel: "checked",
@@ -116,6 +138,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
eab,
skipLocalVerify: this.skipLocalVerify,
useMappingProxy: this.useProxy,
privateKeyType: this.privateKeyType,
});
}
@@ -156,6 +179,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
dnsProvider,
csrInfo,
isTest: false,
privateKeyType: this.privateKeyType,
});
const certInfo = this.formatCerts(cert);

View File

@@ -97,7 +97,7 @@ export class CertApplyLegoPlugin extends CertApplyBasePlugin {
this.http = this.ctx.http;
this.lastStatus = this.ctx.lastStatus as Step;
if (this.legoEabAccessId) {
this.eab = await this.ctx.accessService.getById(this.legoEabAccessId);
this.eab = await this.accessService.getById(this.legoEabAccessId);
}
}
async onInit(): Promise<void> {}

19
packages/ui/.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
logs/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
!dist/
.idea/
run/
.DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*
/data/db.sqlite
*/node_modules
certd-server/tools/windows/

View File

@@ -1,15 +1,15 @@
FROM node:18-alpine AS builder
FROM node:20-alpine AS builder
EXPOSE 7001
WORKDIR /workspace/
COPY . /workspace/
RUN npm install -g pnpm@8.15.7
RUN npm install -g pnpm
RUN cd /workspace/certd-client && pnpm install && npm run build
#RUN cd /workspace/certd-client && pnpm install && npm run build
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
RUN cd /workspace/certd-server && pnpm install && npm run build-on-docker
RUN cp /workspace/certd-client/dist/* /workspace/certd-server/public/ -rf
FROM node:18-alpine
FROM node:20-alpine
WORKDIR /app/
COPY --from=builder /workspace/certd-server/ /app/
RUN chmod +x /app/tools/linux/*

View File

@@ -3,6 +3,65 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.24.0](https://github.com/certd/certd/compare/v1.23.1...v1.24.0) (2024-08-25)
### Bug Fixes
* 部署到腾讯云cdn选择证书任务步骤限制只能选证书 ([3345c14](https://github.com/certd/certd/commit/3345c145b802170f75a098a35d0c4b8312efcd17))
* 修复执行日志没有清理的bug ([22a3363](https://github.com/certd/certd/commit/22a336370a88a7df2a23c967043bae153da71ed5))
### Features
* 支持ECC类型 ([a7424e0](https://github.com/certd/certd/commit/a7424e02f5c7e02ac1688791040785920ce67473))
### Performance Improvements
* 优化成功后跳过的提示 ([7b451bb](https://github.com/certd/certd/commit/7b451bbf6e6337507f4627b5a845f5bd96ab4f7b))
* 优化证书申请成功率 ([968c469](https://github.com/certd/certd/commit/968c4690a07f69c08dcb3d3a494da4e319627345))
* email proxy ([453f1ba](https://github.com/certd/certd/commit/453f1baa0b9eb0f648aa1b71ccf5a95b202ce13f))
## [1.23.1](https://github.com/certd/certd/compare/v1.23.0...v1.23.1) (2024-08-06)
### Performance Improvements
* 优化默认值设置 ([1af19f0](https://github.com/certd/certd/commit/1af19f0ac053fe109782882964533636b5969d6b))
# [1.23.0](https://github.com/certd/certd/compare/v1.22.9...v1.23.0) (2024-08-05)
### Features
* use node 20 ([e8ed972](https://github.com/certd/certd/commit/e8ed97206bf28e83f942db2ef4ea07fa76fd3567))
## [1.22.9](https://github.com/certd/certd/compare/v1.22.8...v1.22.9) (2024-08-05)
### Performance Improvements
* 优化定时任务 ([87e440e](https://github.com/certd/certd/commit/87e440ee2a8b10dc571ce619f28bc83c1e5eb147))
## [1.22.8](https://github.com/certd/certd/compare/v1.22.7...v1.22.8) (2024-08-05)
### Performance Improvements
* 修复删除历史记录没有删除log的bug新增history管理页面演示站点启动时不自动启动非管理员用户的定时任务 ([f78ae93](https://github.com/certd/certd/commit/f78ae93eedfe214008c3d071ca3d77c962137a64))
## [1.22.7](https://github.com/certd/certd/compare/v1.22.6...v1.22.7) (2024-08-04)
**Note:** Version bump only for package @certd/ui-client
## [1.22.6](https://github.com/certd/certd/compare/v1.22.5...v1.22.6) (2024-08-03)
### Bug Fixes
* 修复在相同的cron时偶尔无法触发定时任务的bug ([680941a](https://github.com/certd/certd/commit/680941af119619006b592e3ab6fb112cb5556a8b))
### Performance Improvements
* 流水线支持名称模糊查询 ([59897c4](https://github.com/certd/certd/commit/59897c4ceae992ebe2972ca9e8f9196616ffdfd7))
## [1.22.5](https://github.com/certd/certd/compare/v1.22.4...v1.22.5) (2024-07-26)
**Note:** Version bump only for package @certd/ui-client
## [1.22.3](https://github.com/certd/certd/compare/v1.22.2...v1.22.3) (2024-07-25)
**Note:** Version bump only for package @certd/ui-client

View File

@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.22.3",
"version": "1.24.0",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -24,12 +24,10 @@
"dependencies": {
"@ant-design/colors": "^7.0.2",
"@ant-design/icons-vue": "^7.0.1",
"@aws-sdk/client-s3": "^3.535.0",
"@aws-sdk/s3-request-presigner": "^3.535.0",
"@fast-crud/fast-crud": "^1.21.1",
"@fast-crud/fast-extends": "^1.21.1",
"@fast-crud/ui-antdv4": "^1.21.1",
"@fast-crud/ui-interface": "^1.21.1",
"@fast-crud/fast-crud": "^1.21.2",
"@fast-crud/fast-extends": "^1.21.2",
"@fast-crud/ui-antdv4": "^1.21.2",
"@fast-crud/ui-interface": "^1.21.2",
"@iconify/vue": "^4.1.1",
"@soerenmartius/vue3-clipboard": "^0.1.2",
"ant-design-vue": "^4.1.2",
@@ -59,7 +57,7 @@
"vuedraggable": "^2.24.3"
},
"devDependencies": {
"@certd/pipeline": "^1.22.3",
"@certd/pipeline": "^1.24.0",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",

View File

@@ -1,9 +1,13 @@
import { request } from "../service";
export type SysPublicSetting = {
registerEnabled:boolean
}
registerEnabled: boolean;
managerOtherUserPipeline: boolean;
};
export type SysInstallInfo = {
siteId: string;
};
export async function getSysPublicSettings(): Promise<SysPublicSetting> {
return await request({
@@ -11,3 +15,10 @@ export async function getSysPublicSettings(): Promise<SysPublicSetting> {
method: "get"
});
}
export async function getInstallInfo(): Promise<SysInstallInfo> {
return await request({
url: "/basic/settings/install",
method: "get"
});
}

View File

@@ -18,6 +18,7 @@ export interface UserInfoRes {
id: string | number;
username: string;
nickName: string;
roles: number[];
}
export interface LoginRes {
@@ -59,7 +60,14 @@ export async function mine(): Promise<UserInfoRes> {
});
}
return await request({
url: "/sys/authority/user/mine",
url: "/mine/info",
method: "post"
});
}
export async function getPlusInfo() {
return await request({
url: "/mine/plusInfo",
method: "post"
});
}

View File

@@ -1,15 +1,18 @@
import PiContainer from "./container.vue";
import PiAccessSelector from "../views/certd/access/access-selector/index.vue";
import PiDnsProviderSelector from "./dns-provider-selector/index.vue";
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";import PiEditable from "./editable.vue";
import PiOutputSelector from "../views/certd/pipeline/pipeline/component/output-selector/index.vue";
import PiEditable from "./editable.vue";
import VipButton from "./vip-button/index.vue";
import { CheckCircleOutlined, InfoCircleOutlined, UndoOutlined } from "@ant-design/icons-vue";
export default {
install(app:any) {
install(app: any) {
app.component("PiContainer", PiContainer);
app.component("PiAccessSelector", PiAccessSelector);
app.component("PiEditable", PiEditable);
app.component("PiOutputSelector", PiOutputSelector);
app.component("PiDnsProviderSelector", PiDnsProviderSelector);
app.component("VipButton", VipButton);
app.component("CheckCircleOutlined", CheckCircleOutlined);
app.component("InfoCircleOutlined", InfoCircleOutlined);

View File

@@ -0,0 +1,9 @@
import { request } from "/src/api/service";
export async function doActive(form: any) {
return await request({
url: "/sys/plus/active",
method: "post",
data: form
});
}

View File

@@ -0,0 +1,148 @@
<template>
<div class="layout-vip isPlus">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line"></fs-icon>
<div class="text">
<template v-if="userStore.isPlus">
<a-tooltip>
<template #title> 到期时间{{ expireTime }} </template>
<span @click="openUpgrade">{{ texts.plus }}</span>
</a-tooltip>
</template>
<template v-else>
<a-tooltip>
<template #title> 升级专业版享受更多VIP特权 </template>
<span @click="openUpgrade"> {{ texts.free }} {{ expiredDays }} </span>
</a-tooltip>
</template>
</div>
</div>
</template>
<script lang="tsx" setup>
import { ref, reactive, computed } from "vue";
import { useUserStore } from "/src/store/modules/user";
import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue";
import * as api from "./api";
import { useSettingStore } from "/@/store/modules/settings";
const props = defineProps<{
mode?: "button" | "nav";
}>();
type Texts = {
plus: string;
free: string;
};
const texts = computed<Texts>(() => {
if (props.mode === "button") {
return {
plus: "专业版已开通",
free: "此为专业版功能"
};
} else {
return {
plus: "专业版",
free: "免费版"
};
}
});
const userStore = useUserStore();
const expireTime = computed(() => {
if (userStore.isPlus) {
return dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD");
}
return "";
});
const expiredDays = computed(() => {
if (userStore.plusInfo?.isPlus && !userStore.isPlus) {
//已过期多少天
const days = dayjs().diff(dayjs(userStore.plusInfo.expireTime), "day");
return `专业版已过期${days}`;
}
return "";
});
const formState = reactive({
code: ""
});
async function doActive() {
if (!formState.code) {
message.error("请输入激活码");
throw new Error("请输入激活码");
}
const res = await api.doActive(formState);
if (res) {
await userStore.reInit();
Modal.success({
title: "激活成功",
content: `您已成功激活专业版,有效期至:${dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD")}`
});
}
}
const settingStore = useSettingStore();
const computedSiteId = computed(() => settingStore.installInfo?.siteId);
const [modal, contextHolder] = Modal.useModal();
function openUpgrade() {
const placeholder = "请输入激活码";
modal.confirm({
title: "升级/续期专业版",
async onOk() {
return await doActive();
},
okText: "激活",
width: 500,
content: () => {
return (
<div class="mt-10 mb-10">
<div>
<h3 class="block-header">专业版特权</h3>
<ul>
<li>证书流水线数量无限制</li>
<li>可加VIP群需求优先实现</li>
<li>更多特权敬请期待</li>
</ul>
</div>
<div>
<h3 class="block-header">立刻激活/续期</h3>
<div class="mt-10">
<div class="flex-o w-100">
<span>站点ID</span>
<fs-copyable class="flex-1" v-model={computedSiteId.value}></fs-copyable>
</div>
<a-input class="mt-10" v-model:value={formState.code} placeholder={placeholder} />
</div>
<div class="mt-10">
没有激活码
<a href="https://afdian.com/a/greper" target="_blank">
爱发电赞助VIP会员后获取
</a>
</div>
</div>
</div>
);
}
});
}
</script>
<style lang="less">
.layout-vip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
&.isPlus {
color: #c5913f;
}
.text {
margin-left: 5px;
}
}
</style>

View File

@@ -12,14 +12,14 @@
<a-layout class="layout-body">
<a-layout-header class="header">
<div class="header-buttons">
<div class="header-left header-buttons">
<div class="menu-fold" @click="asideCollapsedToggle">
<MenuUnfoldOutlined v-if="asideCollapsed" />
<MenuFoldOutlined v-else />
</div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<vip-button class="flex-center header-btn"></vip-button>
</div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<div class="header-right header-buttons">
<!-- <button-->
<!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"-->
@@ -83,10 +83,11 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
import FsThemeSet from "/@/layout/components/theme/index.vue";
import { env } from "../utils/util.env";
import FsThemeModeSet from "./components/theme/mode-set.vue";
import VipButton from "/@/components/vip-button/index.vue";
export default {
name: "LayoutFramework",
// eslint-disable-next-line vue/no-unused-components
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet },
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet, VipButton },
setup() {
const resourceStore = useResourceStore();
const frameworkMenus = computed(() => {
@@ -133,6 +134,7 @@ export default {
.fs-framework {
height: 100%;
overflow-x: hidden;
min-width: 1200px;
.menu-fold {
display: flex;
justify-content: center;
@@ -174,34 +176,41 @@ export default {
padding: 5px;
}
}
.header-buttons {
display: flex;
align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
height: 100%;
& > .header-btn {
display: inline-flex;
justify-content: center;
.ant-layout-header.header {
display: flex;
justify-content: space-between;
align-items: center;
.header-buttons {
display: flex;
align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
height: 100%;
//border-bottom: 1px solid rgba(255, 255, 255, 0);
&:hover {
background-color: #fff;
& > .header-btn {
display: inline-flex;
justify-content: center;
align-items: center;
height: 100%;
//border-bottom: 1px solid rgba(255, 255, 255, 0);
&:hover {
background-color: #fff;
}
}
}
.header-right {
justify-content: flex-end;
align-items: center;
display: flex;
}
.header-menu {
flex: 1;
}
}
.header-right {
justify-content: flex-end;
align-items: center;
display: flex;
}
.header-menu {
flex: 1;
}
.aside-menu {
flex: 1;
ui {

View File

@@ -1,7 +1,6 @@
<template>
<div id="userLayout" :class="['user-layout-wrapper']">
<div class="login-container flex-center">
<div class="user-layout-lang"></div>
<div class="user-layout-content">
<div class="top flex flex-col items-center justify-center">
<div class="header flex flex-row items-center">
@@ -146,7 +145,6 @@ export default {
// position: absolute;
width: 100%;
bottom: 0;
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;

View File

@@ -2,7 +2,7 @@ import _ from "lodash-es";
function copyList(originList: any, newList: any, options: any, parentId?: any) {
for (const item of originList) {
const newItem: any = _.cloneDeep(item);
if(parentId!= null && newItem.parentId == null){
if (parentId != null && newItem.parentId == null) {
newItem.parentId = parentId;
}
@@ -228,7 +228,7 @@ const mockUtil: any = {
handle(req: any): any {
const item = findById(req.body.id, list);
if (item) {
_.mergeWith(item, req.body, (objValue, srcValue) => {
_.mergeWith(item, req.body, (objValue: any, srcValue: any) => {
if (srcValue == null) {
return;
}
@@ -305,7 +305,7 @@ const mockUtil: any = {
console.log("req", req);
let item = findById(req.body.id, list);
if (item) {
_.mergeWith(item, { [req.body.key]: req.body.value }, (objValue, srcValue) => {
_.mergeWith(item, { [req.body.key]: req.body.value }, (objValue: any, srcValue: any) => {
if (srcValue == null) {
return;
}
@@ -336,7 +336,7 @@ const mockUtil: any = {
for (const item of req.body) {
const item2 = findById(item.id, list);
if (item2) {
_.mergeWith(item2, item, (objValue, srcValue) => {
_.mergeWith(item2, item, (objValue: any, srcValue: any) => {
if (srcValue == null) {
return;
}

View File

@@ -13,10 +13,10 @@ const list: any = [];
_.forEach(commonMocks, (value: any) => {
list.push(value.default);
});
_.forEach(apiMocks, (value) => {
_.forEach(apiMocks, (value: any) => {
list.push(value.default);
});
_.forEach(viewMocks, (value) => {
_.forEach(viewMocks, (value: any) => {
list.push(value.default);
});

View File

@@ -1,15 +1,37 @@
import { request, requestForMock } from "/src/api/service";
// import "/src/mock";
import { ColumnCompositionProps, CrudOptions, FastCrud, PageQuery, PageRes, setLogger, TransformResProps, useColumns, UseCrudProps, UserPageQuery, useTypes, utils } from "@fast-crud/fast-crud";
import {
ColumnCompositionProps,
CrudOptions,
FastCrud,
PageQuery,
PageRes,
setLogger,
TransformResProps,
useColumns,
UseCrudProps,
UserPageQuery,
useTypes,
utils
} from "@fast-crud/fast-crud";
import "@fast-crud/fast-crud/dist/style.css";
import { FsExtendsCopyable, FsExtendsEditor, FsExtendsJson, FsExtendsTime, FsExtendsUploader, FsExtendsInput, FsUploaderS3SignedUrlType, FsUploaderGetAuthContext, FsUploaderAliossSTS } from "@fast-crud/fast-extends";
import {
FsExtendsCopyable,
FsExtendsEditor,
FsExtendsJson,
FsExtendsTime,
FsExtendsUploader,
FsExtendsInput,
FsUploaderS3SignedUrlType,
FsUploaderGetAuthContext,
FsUploaderAliossSTS
} from "@fast-crud/fast-extends";
import "@fast-crud/fast-extends/dist/style.css";
import UiAntdv from "@fast-crud/ui-antdv4";
import "@fast-crud/ui-antdv4/dist/style.css";
import _ from "lodash-es";
import { useCrudPermission } from "../permission";
import { App } from "vue";
import { GetSignedUrl } from "/@/views/crud/component/uploader/s3/api";
import { notification } from "ant-design-vue";
function install(app: App, options: any = {}) {
@@ -39,7 +61,7 @@ function install(app: App, options: any = {}) {
},
size: "small",
pagination: false,
onResizeColumn: (w: number, col: any) => {
onResizeColumn: (w: number | string, col: any) => {
if (crudBinding.value?.table?.columnsMap && crudBinding.value?.table?.columnsMap[col.key]) {
crudBinding.value.table.columnsMap[col.key].width = w;
}
@@ -149,6 +171,7 @@ function install(app: App, options: any = {}) {
// fast-extends里面的扩展组件均为异步组件只有在使用时才会被加载并不会影响首页加载速度
//安装uploader 公共参数
// @ts-ignore
app.use(FsExtendsUploader, {
// @ts-ignore
defaultType: "cos",
@@ -231,33 +254,6 @@ function install(app: App, options: any = {}) {
},
domain: "http://d2p.file.handsfree.work/"
},
s3: {
keepName: true,
//同时也支持minio
bucket: "fast-crud",
sdkOpts: {
s3ForcePathStyle: true,
signatureVersion: "v4",
region: "us-east-1",
forcePathStyle: true,
//minio与s3完全适配
endpoint: "https://play.min.io",
credentials: {
//不建议在客户端使用secretAccessKey来上传
accessKeyId: "Q3AM3UQ867SPQQA43P2F", //访问登录名
secretAccessKey: "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG" //访问密码
}
},
//预签名配置,向后端获取上传的预签名连接
async getSignedUrl(bucket: string, key: string, options: any, type: FsUploaderS3SignedUrlType = "put") {
return await GetSignedUrl(bucket, key, type);
},
successHandle(ret: any) {
// 上传完成后可以在此处处理结果修改url什么的
console.log("success handle:", ret);
return ret;
}
},
form: {
keepName: true,
action: "http://www.docmirror.cn:7070/api/upload/form/upload",
@@ -340,6 +336,23 @@ function install(app: App, options: any = {}) {
return columnProps;
}
});
registerMergeColumnPlugin({
name: "resize-column-plugin",
order: 2,
handle: (columnProps: ColumnCompositionProps) => {
if (!columnProps.column) {
columnProps.column = {};
}
columnProps.column.resizable = true;
if (!columnProps.column.width) {
columnProps.column.width = 100;
} else if (typeof columnProps.column?.width === "string" && columnProps.column.width.indexOf("px") > -1) {
columnProps.column.width = parseInt(columnProps.column.width.replace("px", ""));
}
return columnProps;
}
});
}
export default {

View File

@@ -33,6 +33,7 @@ router.beforeEach(async (to, from, next) => {
// 请根据自身业务需要修改
const token = userStore.getToken;
if (token) {
await userStore.init();
next();
} else {
// 没有登录的时候跳转到登录界面

View File

@@ -27,6 +27,15 @@ export const certdResources = [
isMenu: false
}
},
{
title: "执行历史记录",
name: "pipelineHistory",
path: "/certd/history",
component: "/certd/history/index.vue",
meta: {
icon: "ion:timer-outline"
}
},
{
title: "授权管理",
name: "access",

View File

@@ -1,784 +0,0 @@
export const crudResources = [
{
title: "CRUD示例",
name: "crud",
path: "/crud",
redirect: "/crud/basis",
meta: {
icon: "ion:apps-sharp"
},
children: [
{
title: "debug",
name: "debug",
path: "/crud/debug",
component: "/crud/debug/index.vue",
meta: {
isMenu: false
}
},
{
title: "基本特性",
name: "basis",
path: "/crud/basis",
redirect: "/crud/basis/i18n",
meta: {
icon: "ion:disc-outline"
},
children: [
{
title: "FirstDemo",
name: "FsCrudFirst",
path: "/crud/basis/first",
component: "/crud/basis/first/index.vue"
},
{
title: "HelloWorld",
name: "FsCrudHelloWorld",
path: "/crud/basis/helloworld",
component: "/crud/basis/helloworld/index.vue"
},
{
title: "动态计算",
name: "BasisCompute",
path: "/crud/basis/compute",
component: "/crud/basis/compute/index.vue"
},
{
title: "动态计算-更多示例",
name: "BasisComputeMore",
path: "/crud/basis/compute-more",
component: "/crud/basis/compute-more/index.vue"
},
{
title: "国际化",
name: "BasisI18n",
path: "/crud/basis/i18n",
component: "/crud/basis/i18n/index.vue"
},
{
title: "ValueChange",
name: "BasisValueChange",
path: "/crud/basis/value-change",
component: "/crud/basis/value-change/index.vue"
},
{
title: "Card布局",
name: "BasisLayoutCard",
path: "/crud/basis/layout-card",
component: "/crud/basis/layout-card/index.vue"
},
{
title: "自定义布局",
name: "BasisLayoutCustom",
path: "/crud/basis/layout-custom",
component: "/crud/basis/layout-custom/index.vue"
},
{
title: "自定义组件",
name: "BasisCustom",
path: "/crud/basis/custom",
component: "/crud/basis/custom/index.vue"
},
{
title: "列设置",
name: "BasisColumnsSet",
path: "/crud/basis/columns-set",
component: "/crud/basis/columns-set/index.vue"
},
{
title: "字段合并插件",
name: "BasisColumnMergePlugin",
path: "/crud/basis/column-merge-plugin",
component: "/crud/basis/column-merge-plugin/index.vue"
},
{
title: "ResetCrudOptions",
name: "BasisReset",
path: "/crud/basis/reset",
component: "/crud/basis/reset/index.vue",
meta: {
cache: true
}
},
{
title: "CrudOptions插件",
name: "BasisPlugin",
path: "/crud/basis/plugin",
component: "/crud/basis/plugin/index.vue"
},
{
title: "Ts定义测试",
name: "BasisTsTest",
path: "/crud/basis/ts",
component: "/crud/basis/ts/index.vue"
}
]
},
{
title: "数据字典",
name: "dict",
path: "/crud/dict",
redirect: "/crud/dict/single",
meta: {
icon: "ion:book-outline"
},
children: [
{
title: "单例",
name: "DictSingle",
path: "/crud/dict/single",
component: "/crud/dict/single/index.vue"
},
{
title: "分发复制",
name: "DictCloneable",
path: "/crud/dict/cloneable",
component: "/crud/dict/cloneable/index.vue"
},
{
title: "原型复制",
name: "DictPrototype",
path: "/crud/dict/prototype",
component: "/crud/dict/prototype/index.vue"
},
{
title: "页面间共享",
name: "DictShared",
path: "/crud/dict/shared",
children: [
{
title: "共享字典数据管理",
name: "DictSharedManager",
path: "/crud/dict/shared/manager",
component: "/crud/dict/shared/manager/index.vue"
},
{
title: "共享字典使用",
name: "DictSharedUse",
path: "/crud/dict/shared/use",
component: "/crud/dict/shared/use/index.vue"
}
]
}
]
},
{
title: "操作列",
name: "row-handle",
path: "/crud/row-handle",
redirect: "/crud/row-handle/tooltip",
meta: {
icon: "ion:build-outline"
},
children: [
{
title: "Tooltip",
name: "RowHandleTooltip",
path: "/crud/row-handle/tooltip",
component: "/crud/row-handle/tooltip/index.vue"
},
{
title: "按钮折叠",
name: "RowHandleDropdown",
path: "/crud/row-handle/dropdown",
component: "/crud/row-handle/dropdown/index.vue"
}
]
},
{
title: "组件示例",
name: "component",
path: "/crud/component",
redirect: "/crud/component/text",
meta: {
icon: "ion:cube-outline"
},
children: [
{
title: "文本输入(input)",
name: "ComponentText",
path: "/crud/component/text",
component: "/crud/component/text/index.vue"
},
{
title: "选择(select)",
name: "ComponentSelect",
path: "/crud/component/select",
component: "/crud/component/select/index.vue"
},
{
title: " 表格选择(table-select)",
name: "ComponentTableSelect",
path: "/crud/component/table-select",
component: "/crud/component/table-select/index.vue"
},
{
title: "级联(cascader)",
name: "ComponentCascader",
path: "/crud/component/cascader",
component: "/crud/component/cascader/index.vue"
},
{
title: "多选(checkbox)",
name: "ComponentCheckbox",
path: "/crud/component/checkbox",
component: "/crud/component/checkbox/index.vue"
},
{
title: "单选(radio)",
name: "ComponentRadio",
path: "/crud/component/radio",
component: "/crud/component/radio/index.vue"
},
{
title: "开关(switch)",
name: "ComponentSwitch",
path: "/crud/component/switch",
component: "/crud/component/switch/index.vue"
},
{
title: "日期时间(date)",
name: "ComponentDate",
path: "/crud/component/date",
component: "/crud/component/date/index.vue"
},
{
title: "按钮链接",
name: "ComponentButton",
path: "/crud/component/button",
component: "/crud/component/button/index.vue"
},
{
title: "数字",
name: "ComponentNumber",
path: "/crud/component/number",
component: "/crud/component/number/index.vue"
},
{
title: "树形选择",
name: "ComponentTree",
path: "/crud/component/tree",
component: "/crud/component/tree/index.vue"
},
{
title: "图片裁剪上传",
name: "ComponentUploaderCropper",
path: "/crud/component/uploader/cropper",
component: "/crud/component/uploader/cropper/index.vue"
},
{
title: "表单本地上传",
name: "ComponentUploaderForm",
path: "/crud/component/uploader/form",
component: "/crud/component/uploader/form/index.vue"
},
{
title: "阿里云oss上传",
name: "ComponentUploaderAlioss",
path: "/crud/component/uploader/alioss",
component: "/crud/component/uploader/alioss/index.vue"
},
{
title: "腾讯云cos上传",
name: "ComponentUploaderCos",
path: "/crud/component/uploader/cos",
component: "/crud/component/uploader/cos/index.vue"
},
{
title: "七牛云上传",
name: "ComponentUploaderQiniu",
path: "/crud/component/uploader/qiniu",
component: "/crud/component/uploader/qiniu/index.vue"
},
{
title: "s3上传",
name: "ComponentUploaderS3",
path: "/crud/component/uploader/s3",
component: "/crud/component/uploader/s3/index.vue"
},
{
title: "富文本编辑器",
name: "ComponentEditor",
path: "/crud/component/editor",
component: "/crud/component/editor/index.vue"
},
{
title: "图标",
name: "ComponentIcon",
path: "/crud/component/icon",
component: "/crud/component/icon/index.vue"
},
{
title: "JsonEditor",
name: "ComponentJson",
path: "/crud/component/json",
component: "/crud/component/json/index.vue"
},
{
title: "手机号输入框",
name: "ComponentPhone",
path: "/crud/component/phone",
component: "/crud/component/phone/index.vue"
},
{
title: "组件独立使用",
name: "ComponentIndependent",
path: "/crud/component/independent",
component: "/crud/component/independent/index.vue"
}
]
},
{
title: "Form表单",
name: "form",
path: "/crud/form",
redirect: "/crud/form/layout",
meta: {
icon: "ion:document-text-outline"
},
children: [
{
title: "基本表单",
name: "FormBase",
path: "/crud/form/base",
component: "/crud/form/base/index.vue"
},
{
title: "表单Grid布局",
name: "FormLayoutGrid",
path: "/crud/form/layout-grid",
component: "/crud/form/layout-grid/index.vue"
},
{
title: "表单Flex布局",
name: "FormLayoutFlex",
path: "/crud/form/layout-flex",
component: "/crud/form/layout-flex/index.vue"
},
{
title: "表单动态布局",
name: "FormLayout",
path: "/crud/form/layout",
component: "/crud/form/layout/index.vue"
},
{
title: "表单单列模式",
name: "FormSingleColumn",
path: "/crud/form/single-column",
component: "/crud/form/single-column/index.vue"
},
{
title: "表单校验",
name: "FormValidation",
path: "/crud/form/validation",
component: "/crud/form/validation/index.vue"
},
{
title: "抽屉表单",
name: "FormDrawer",
path: "/crud/form/drawer",
component: "/crud/form/drawer/index.vue"
},
{
title: "表单分组",
name: "FormGroup",
path: "/crud/form/group",
component: "/crud/form/group/index.vue"
},
{
title: "表单分组(tabs)",
name: "FormGroupTabs",
path: "/crud/form/group-tabs",
component: "/crud/form/group-tabs/index.vue"
},
{
title: "自定义表单",
name: "FormCustomForm",
path: "/crud/form/custom-form",
component: "/crud/form/custom-form/index.vue"
},
{
title: "字段帮助说明",
name: "FormHelper",
path: "/crud/form/helper",
component: "/crud/form/helper/index.vue"
},
{
title: "页面内部弹出表单",
name: "FormInner",
path: "/crud/form/inner",
component: "/crud/form/inner/index.vue",
meta: {
cache: true
}
},
{
title: "地区字典管理",
name: "FormInnerArea",
path: "/crud/form/inner/area",
component: "/crud/form/inner/area/index.vue",
meta: {
isMenu: false
}
},
{
title: "新页面编辑",
name: "FormNewPage",
path: "/crud/form/new-page",
component: "/crud/form/new-page/index.vue",
meta: {
cache: false
}
},
{
title: "新页面编辑表单",
name: "FormNewPageEdit",
path: "/crud/form/new-page/edit",
component: "/crud/form/new-page/edit.vue",
meta: {
isMenu: false
}
},
{
title: "独立使用表单",
name: "FormIndependent",
path: "/crud/form/independent",
component: "/crud/form/independent/index.vue"
},
{
title: "重置表单",
name: "FormReset",
path: "/crud/form/reset",
component: "/crud/form/reset/index.vue"
},
{
title: "嵌套数据结构",
name: "FormNest",
path: "/crud/form/nest",
component: "/crud/form/nest/index.vue"
},
{
title: "字段组件render",
name: "FormRender",
path: "/crud/form/render",
component: "/crud/form/render/index.vue"
},
{
title: "查看表单使用单元格组件",
name: "FormView",
path: "/crud/form/view",
component: "/crud/form/view/index.vue"
},
{
title: "initialForm",
name: "FormInitial",
path: "/crud/form/initial",
component: "/crud/form/initial/index.vue"
},
{
title: "表单Watch",
name: "FormWatch",
path: "/crud/form/watch",
component: "/crud/form/watch/index.vue"
}
]
},
{
title: "表格特性",
path: "/crud/feature",
meta: {
icon: "ion:beer-outline"
},
redirect: "/crud/feature/dropdown",
children: [
{
title: "部件显隐",
name: "FeatureHide",
path: "/crud/feature/hide",
component: "/crud/feature/hide/index.vue"
},
{
title: "多选&批量删除",
name: "FeatureSelection",
path: "/crud/feature/selection",
component: "/crud/feature/selection/index.vue"
},
{
title: "单选",
name: "FeatureSelectionRadio",
path: "/crud/feature/selection-radio",
component: "/crud/feature/selection-radio/index.vue"
},
{
title: "表头过滤",
name: "FeatureFilter",
path: "/crud/feature/filter",
component: "/crud/feature/filter/index.vue"
},
{
title: "行展开",
name: "FeatureExpand",
path: "/crud/feature/expand",
component: "/crud/feature/expand/index.vue"
},
{
title: "树形表格",
name: "FeatureTree",
path: "/crud/feature/tree",
component: "/crud/feature/tree/index.vue"
},
{
title: "多级表头",
name: "FeatureHeaderGroup",
path: "/crud/feature/header-group",
component: "/crud/feature/header-group/index.vue"
},
{
title: "自定义表头",
name: "FeatureHeader",
path: "/crud/feature/header",
component: "/crud/feature/header/index.vue"
},
{
title: "合并单元格",
name: "FeatureMerge",
path: "/crud/feature/merge",
component: "/crud/feature/merge/index.vue"
},
{
title: "序号",
name: "FeatureIndex",
path: "/crud/feature/index",
component: "/crud/feature/index/index.vue"
},
{
title: "排序",
name: "FeatureSortable",
path: "/crud/feature/sortable",
component: "/crud/feature/sortable/index.vue"
},
{
title: "固定列",
name: "FeatureFixed",
path: "/crud/feature/fixed",
component: "/crud/feature/fixed/index.vue"
},
{
title: "不固定高度",
name: "FeatureHeight",
path: "/crud/feature/height",
component: "/crud/feature/height/index.vue"
},
{
title: "查询框",
name: "FeatureSearch",
path: "/crud/feature/search",
component: "/crud/feature/search/index.vue"
},
{
title: "查询框多行模式",
name: "FeatureSearchMulti",
path: "/crud/feature/search-multi",
component: "/crud/feature/search-multi/index.vue"
},
{
title: "Tabs快捷查询",
name: "FeatureTabs",
path: "/crud/feature/tabs",
component: "/crud/feature/tabs/index.vue"
},
{
title: "字段排序",
name: "FeatureColumnSort",
path: "/crud/feature/column-sort",
component: "/crud/feature/column-sort/index.vue"
},
{
title: "ValueBuilder",
name: "FeatureValueBuilder",
path: "/crud/feature/value-builder",
component: "/crud/feature/value-builder/index.vue"
},
{
title: "列设置",
name: "FeatureColumnsSet",
path: "/crud/feature/columns-set",
component: "/crud/feature/columns-set/index.vue"
},
{
title: "本地化编辑",
name: "FeatureLocal",
path: "/crud/feature/local",
component: "/crud/feature/local/index.vue"
},
{
title: "v-model",
name: "FeatureVModel",
path: "/crud/feature/local-v-model",
component: "/crud/feature/local-v-model/index.vue"
},
{
title: "导入",
name: "FeatureImport",
path: "/crud/feature/local-import",
component: "/crud/feature/local-import/index.vue"
},
{
title: "导出",
name: "FeatureExport",
path: "/crud/feature/export",
component: "/crud/feature/export/index.vue"
},
{
title: "自定义删除",
name: "FeatureRemove",
path: "/crud/feature/remove",
component: "/crud/feature/remove/index.vue"
},
{
title: "调整列宽",
name: "FeatureColumnResize",
path: "/crud/feature/column-resize",
component: "/crud/feature/column-resize/index.vue"
}
]
},
{
title: "可编辑",
name: "Editable",
path: "/crud/editable",
redirect: "/crud/editable/free",
meta: {
icon: "ion:create-outline"
},
children: [
{
title: "自由编辑",
name: "EditableFree",
path: "/crud/editable/free",
component: "/crud/editable/free/index.vue"
},
{
title: "行编辑",
name: "EditableRow",
path: "/crud/editable/row",
component: "/crud/editable/row/index.vue"
},
{
title: "单元格编辑",
name: "EditableCell",
path: "/crud/editable/cell",
component: "/crud/editable/cell/index.vue"
},
{
title: "子表格编辑",
name: "EditableVModel",
path: "/crud/editable/vmodel",
component: "/crud/editable/vmodel/index.vue"
},
{
title: "子CRUD",
name: "EditableSubCrud",
path: "/crud/editable/sub-crud",
component: "/crud/editable/sub-crud/index.vue"
}
]
},
{
title: "插槽",
name: "Slots",
path: "/crud/slots",
redirect: "/crud/slots/layout",
meta: {
icon: "ion:extension-puzzle-outline"
},
children: [
{
title: "页面占位插槽",
name: "SlotsLayout",
path: "/crud/slots/layout",
component: "/crud/slots/layout/index.vue"
},
{
title: "表单占位插槽",
name: "SlotsForm",
path: "/crud/slots/form",
component: "/crud/slots/form/index.vue"
},
{
title: "查询字段插槽",
name: "SlotsSearch",
path: "/crud/slots/search",
component: "/crud/slots/search/index.vue"
},
{
title: "单元格插槽",
name: "SlotsCell",
path: "/crud/slots/cell",
component: "/crud/slots/cell/index.vue"
},
{
title: "表单字段插槽",
name: "SlotsFormItem",
path: "/crud/slots/form-item",
component: "/crud/slots/form-item/index.vue"
}
]
},
{
title: "复杂需求",
name: "Advanced",
path: "/crud/advanced",
redirect: "/crud/advanced/linkage",
meta: {
icon: "ion:flame-outline"
},
children: [
{
title: "选择联动",
name: "AdvancedLinkage",
path: "/crud/advanced/linkage",
component: "/crud/advanced/linkage/index.vue"
},
{
title: "后台加载crud",
name: "AdvancedFormBackend",
path: "/crud/advanced/from-backend",
component: "/crud/advanced/from-backend/index.vue"
},
{
title: "本地分页",
name: "AdvancedLocalPagination",
path: "/crud/advanced/local-pagination",
component: "/crud/advanced/local-pagination/index.vue"
},
{
title: "嵌套子表格",
name: "AdvancedNest",
path: "/crud/advanced/nest",
component: "/crud/advanced/nest/index.vue"
},
{
title: "对话框中显示crud",
name: "AdvancedInDialog",
path: "/crud/advanced/in-dialog",
component: "/crud/advanced/in-dialog/index.vue"
},
{
title: "抽屉中显示crud",
name: "AdvancedInDrawer",
path: "/crud/advanced/in-drawer",
component: "/crud/advanced/in-drawer/index.vue"
},
{
title: "大量数据",
name: "AdvancedBigData",
path: "/crud/advanced/big-data",
component: "/crud/advanced/big-data/index.vue"
}
]
}
]
}
];

View File

@@ -1,22 +0,0 @@
export const integrationResources = [
{
title: "集成",
name: "integration",
path: "/integration",
redirect: "/integration/bpmn",
meta: {
icon: "ion:apps-sharp"
},
children: [
{
title: "FsBpmn",
name: "FsBpmn",
path: "/integration/bpmn",
component: "/integration/bpmn/index.vue",
meta: {
icon: "ion:disc-outline"
}
}
]
}
];

View File

@@ -1,30 +0,0 @@
export const uiResources = [
{
title: "UI示例",
name: "ui",
path: "/ui",
redirect: "/ui/form",
meta: {
icon: "ion:apps-sharp"
},
children: [
{
title: "表单组件",
name: "UIForm",
path: "/ui/form",
redirect: "/ui/form/input",
meta: {
icon: "ion:disc-outline"
},
children: [
{
title: "input",
name: "UIFormInput",
path: "/ui/form/input",
component: "/ui/form/input/index.vue"
}
]
}
]
}
];

View File

@@ -4,9 +4,8 @@ import _ from "lodash-es";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
import { SysPublicSetting } from "/@/api/modules/api.basic";
import * as basicApi from '/@/api/modules/api.basic'
import _ from "lodash-es";
import * as basicApi from "/@/api/modules/api.basic";
import { SysInstallInfo, SysPublicSetting } from "/@/api/modules/api.basic";
export type ThemeToken = {
token: {
@@ -21,7 +20,10 @@ export type ThemeConfig = {
export interface SettingState {
themeConfig?: ThemeConfig;
themeToken: ThemeToken;
sysPublic?: SysPublicSetting
sysPublic?: SysPublicSetting;
installInfo?: {
siteId: string;
};
}
const defaultThemeConfig = {
@@ -38,21 +40,31 @@ export const useSettingStore = defineStore({
algorithm: theme.defaultAlgorithm
},
sysPublic: {
registerEnabled: false
registerEnabled: false,
managerOtherUserPipeline: false
},
installInfo: {
siteId: ""
}
}),
getters: {
getThemeConfig(): any {
return this.themeConfig || _.merge({}, defaultThemeConfig, LocalStorage.get(SETTING_THEME_KEY) || {});
},
getSysPublic():SysPublicSetting{
return this.sysPublic
getSysPublic(): SysPublicSetting {
return this.sysPublic;
},
getInstallInfo(): SysInstallInfo {
return this.installInfo;
}
},
actions: {
async loadSysSettings(){
const settings = await basicApi.getSysPublicSettings()
_.merge(this.sysPublic,settings)
async loadSysSettings() {
const settings = await basicApi.getSysPublicSettings();
_.merge(this.sysPublic, settings);
const installInfo = await basicApi.getInstallInfo();
_.merge(this.installInfo, installInfo);
},
persistThemeConfig() {
LocalStorage.set(SETTING_THEME_KEY, this.getThemeConfig);
@@ -92,7 +104,7 @@ export const useSettingStore = defineStore({
},
async init() {
await this.setThemeConfig(this.getThemeConfig);
await this.loadSysSettings()
await this.loadSysSettings();
}
}
});

View File

@@ -1,21 +1,28 @@
import { defineStore } from "pinia";
import { store } from "../index";
import router from "../../router";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
// @ts-ignore
import * as UserApi from "/src/api/modules/api.user";
import { RegisterReq } from "/src/api/modules/api.user";
// @ts-ignore
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { mitter } from "/src/utils/util.mitt";
import { RegisterReq } from "/src/api/modules/api.user";
interface UserState {
userInfo: Nullable<UserInfoRes>;
token?: string;
plusInfo?: PlusInfo;
inited: boolean;
}
interface PlusInfo {
vipType: string;
expireTime: number;
isPlus: boolean;
}
const USER_INFO_KEY = "USER_INFO";
@@ -26,7 +33,10 @@ export const useUserStore = defineStore({
// user info
userInfo: null,
// token
token: undefined
token: undefined,
// plus
plusInfo: null,
inited: false
}),
getters: {
getUserInfo(): UserInfoRes {
@@ -34,6 +44,12 @@ export const useUserStore = defineStore({
},
getToken(): string {
return this.token || LocalStorage.get(TOKEN_KEY);
},
isAdmin(): boolean {
return this.getUserInfo?.id === 1;
},
isPlus(): boolean {
return this.plusInfo?.isPlus && this.plusInfo?.expireTime > new Date().getTime();
}
},
actions: {
@@ -70,10 +86,7 @@ export const useUserStore = defineStore({
// save token
this.setToken(token, expire);
// get user info
const userInfo = await this.getUserInfoAction();
await router.replace("/");
mitter.emit("app.login", { userInfo, token: data });
return userInfo;
return await this.onLoginSuccess(data);
} catch (error) {
return null;
}
@@ -83,6 +96,19 @@ export const useUserStore = defineStore({
this.setUserInfo(userInfo);
return userInfo;
},
async onLoginSuccess(loginData: any) {
await this.getUserInfoAction();
await this.loadPlusInfo();
const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { userInfo, token: loginData, plusInfo: this.plusInfo });
await router.replace("/");
return userInfo;
},
async loadPlusInfo() {
this.plusInfo = await UserApi.getPlusInfo();
},
/**
* @description: logout
*/
@@ -105,6 +131,19 @@ export const useUserStore = defineStore({
await this.logout(true);
}
});
},
async init() {
if (this.inited) {
return;
}
if (this.getToken) {
await this.loadPlusInfo();
}
this.inited = true;
},
async reInit() {
this.inited = false;
await this.init();
}
}
});

View File

@@ -14,9 +14,6 @@ html, body {
box-sizing: border-box;
}
body{
min-width: 1000px;
}
div#app {
height: 100%
}
@@ -48,6 +45,11 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important;
}
.flex-center{
display: flex;
justify-content: center;
align-items: center;
}
.flex-o{
display: flex !important;
align-items: center;
@@ -108,3 +110,10 @@ h1, h2, h3, h4, h5, h6 {
.w-100{
width: 100%;
}
.block-header{
margin:3px;
padding-top: 15px;
padding-bottom:3px;
border-bottom: 1px solid #dedede;
}

View File

@@ -6,12 +6,10 @@ export function useReference(form: any) {
return;
}
for (const reference of form.reference) {
debugger;
_.set(
form,
reference.dest,
compute<any>((scope) => {
debugger;
return _.get(scope, reference.src);
})
);

View File

@@ -1,10 +1,11 @@
import { requestForMock } from "/src/api/service";
const request = requestForMock;
const apiPrefix = "/mock/FeatureHeader";
import { request } from "/src/api/service";
const apiPrefix = "/pi/history";
export function GetList(query: any) {
return request({
url: apiPrefix + "/page",
method: "get",
method: "post",
data: query
});
}
@@ -35,24 +36,24 @@ export function DelObj(id: any) {
export function GetObj(id: any) {
return request({
url: apiPrefix + "/get",
method: "get",
url: apiPrefix + "/info",
method: "post",
params: { id }
});
}
export function BatchDelete(ids: any[]) {
export function GetDetail(id: any) {
return request({
url: apiPrefix + "/batchDelete",
url: apiPrefix + "/detail",
method: "post",
params: { id }
});
}
export function DeleteBatch(ids: any[]) {
return request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids }
});
}
export function ColumnUpdate(key: string, value: any) {
return request({
url: apiPrefix + "/columnUpdate",
method: "post",
data: { key, value }
});
}

View File

@@ -0,0 +1,154 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/modules/user";
import { useSettingStore } from "/@/store/modules/settings";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
form.content = JSON.stringify({
title: form.title
});
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件生成行选择crudOptions配置最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys
}
}
}
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
actionbar: {
buttons: {
add: {
show: false
}
}
},
rowHandle: {
minWidth: 200,
fixed: "right",
buttons: {
edit: {
show: false
}
}
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 100
},
form: {
show: false
}
},
userId: {
title: "用户Id",
type: "number",
search: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
})
},
form: {
show: false
},
column: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
})
}
},
pipelineId: {
title: "流水线Id",
type: "number",
search: {
show: true
},
form: {
show: false
}
},
pipelineTitle: {
title: "流水线名称",
type: "link",
search: {
show: true,
component: {
name: "a-input"
}
},
column: {
width: 200
}
},
createTime: {
title: "创建时间",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 125,
align: "center"
}
},
updateTime: {
title: "更新时间",
type: "datetime",
form: {
show: false
},
column: {
show: true
}
}
}
}
};
}

View File

@@ -0,0 +1,51 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">流水线执行记录</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip title="批量删除">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</fs-crud>
</fs-page>
</template>
<script lang="ts" setup>
import { onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "/@/views/certd/history/api";
defineOptions({
name: "PipelineHistory"
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: "确认",
content: `确定要批量删除这${selectedRowKeys.value.length}条记录吗`,
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info("删除成功");
crudExpose.doRefresh();
selectedRowKeys.value = [];
}
});
} else {
message.error("请先勾选记录");
}
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
</script>
<style lang="less"></style>

View File

@@ -1,6 +1,6 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { ref } from "vue";
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { statusUtil } from "/@/views/certd/pipeline/pipeline/utils/util.status";
@@ -8,6 +8,8 @@ import { nanoid } from "nanoid";
import { message, Modal } from "ant-design-vue";
import { env } from "/@/utils/util.env";
import { useUserStore } from "/@/store/modules/user";
import dayjs from "dayjs";
import { useSettingStore } from "/@/store/modules/settings";
export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
@@ -93,6 +95,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
});
}
const userStore = useUserStore();
const settingStore = useSettingStore();
return {
crudOptions: {
request: {
@@ -125,6 +128,8 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}
},
rowHandle: {
minWidth: 200,
fixed: "right",
buttons: {
view: {
click({ row }) {
@@ -182,6 +187,9 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
title: "ID",
key: "id",
type: "number",
search: {
show: true
},
column: {
width: 50
},
@@ -189,6 +197,23 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
show: false
}
},
userId: {
title: "用户Id",
type: "number",
search: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
})
},
form: {
show: false
},
column: {
show: computed(() => {
return userStore.isAdmin && settingStore.sysPublic.managerOtherUserPipeline;
})
}
},
title: {
title: "流水线名称",
type: "link",
@@ -200,6 +225,7 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
},
column: {
width: 300,
sorter: true,
component: {
on: {
// 注意必须要on前缀
@@ -210,11 +236,60 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}
}
},
content: {
title: "定时任务数量",
type: "number",
column: {
cellRender({ value }) {
if (value && value.triggers) {
return value.triggers?.length > 0 ? value.triggers.length : "-";
}
return "-";
}
},
valueBuilder({ row }) {
if (row.content) {
row.content = JSON.parse(row.content);
}
},
valueResolve({ row }) {
if (row.content) {
row.content = JSON.stringify(row.content);
}
},
form: {
show: false
}
},
lastVars: {
title: "到期剩余",
type: "number",
form: {
show: false
},
column: {
cellRender({ row }) {
if (!row.lastVars?.certExpiresTime) {
return "-";
}
const leftDays = dayjs(row.lastVars.certExpiresTime).diff(dayjs(), "day");
const color = leftDays < 20 ? "red" : "#389e0d";
const percent = (leftDays / 90) * 100;
return <a-progress percent={percent} strokeColor={color} format={(percent: number) => `${leftDays}`} />;
},
width: 110
}
},
lastHistoryTime: {
title: "最后运行",
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 120,
align: "center"
}
},
status: {
@@ -225,6 +300,11 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}),
form: {
show: false
},
column: {
sorter: true,
width: 80,
align: "center"
}
},
@@ -242,6 +322,9 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
show: false
},
column: {
sorter: true,
width: 80,
align: "center",
component: {
name: "fs-dict-switch",
vModel: "checked"
@@ -254,12 +337,28 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
}
}
},
keepHistoryCount: {
title: "历史记录保持数",
type: "number",
form: {
value: 30,
value: 20,
helper: "历史记录保持条数,多余的会被删除"
},
column: {
show: false
}
},
order: {
title: "排序号",
type: "number",
column: {
sorter: true,
align: "center",
width: 80
},
form: {
value: 0
}
},
createTime: {
@@ -267,6 +366,11 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
type: "datetime",
form: {
show: false
},
column: {
sorter: true,
width: 125,
align: "center"
}
},
updateTime: {
@@ -274,6 +378,9 @@ export default function ({ crudExpose, context: { certdFormRef } }: CreateCrudOp
type: "datetime",
form: {
show: false
},
column: {
show: false
}
}
}

View File

@@ -12,6 +12,9 @@ export default {
modelValue: {
type: String,
default: undefined
},
from: {
type: String
}
},
emits: ["update:modelValue"],
@@ -23,7 +26,7 @@ export default {
const currentStepIndex = inject("currentStepIndex") as Ref<number>;
const currentTask = inject("currentTask") as Ref<any>;
const getPluginGroups = inject("getPluginGroups") as Ref<any>;
const getPluginGroups = inject("getPluginGroups") as any;
const pluginGroups = getPluginGroups();
function onCreate() {
options.value = pluginGroups.getPreStepOutputOptions({
@@ -32,6 +35,9 @@ export default {
currentStepIndex: currentStepIndex.value,
currentTask: currentTask.value
});
if (props.from) {
options.value = options.value.filter((item: any) => item.type === props.from);
}
if (props.modelValue == null && options.value.length > 0) {
ctx.emit("update:modelValue", options.value[0].value);
}
@@ -42,7 +48,7 @@ export default {
watch(
() => {
return pluginGroups.value.map;
return pluginGroups.value?.map;
},
() => {
onCreate();

View File

@@ -61,24 +61,7 @@
<fs-form-item v-model="currentStep.input[key]" :item="item" :get-context-fn="blankFn" />
</template>
<fs-form-item
v-model="currentStep.strategy.runStrategy"
:item="{
title: '运行策略',
key: 'strategy.runStrategy',
component: {
name: 'a-select',
vModel: 'value',
options: [
{ value: 0, label: '正常运行(证书申请任务请选择它)' },
{ value: 1, label: '成功后跳过(非证书任务请选择它)' }
]
},
helper: '该任务运行成功一次之后下次运行是否跳过,保持默认即可',
rules: [{ required: true, message: '此项必填' }]
}"
:get-context-fn="blankFn"
/>
<fs-form-item v-model="currentStep.strategy.runStrategy" :item="runStrategyProps" :get-context-fn="blankFn" />
</a-form>
<template #footer>
@@ -148,15 +131,8 @@ export default {
// 给step的input设置默认值
changeCurrentPlugin(currentStep.value);
//赋初始
//合并默认
_.merge(currentStep.value, { input: {}, strategy: { runStrategy: 0 } }, currentPlugin.value.default, currentStep.value);
for (const key in currentPlugin.value.input) {
const input = currentPlugin.value.input[key];
if (input.default != null) {
currentStep.value.input[key] = input.default ?? input.value;
}
}
};
const stepDrawerShow = () => {
@@ -229,7 +205,7 @@ export default {
}
}
//设置初始值
if (input.default != null && currentStep.value.input[key] == null) {
if ((input.default != null || input.value != null) && currentStep.value.input[key] == null) {
currentStep.value.input[key] = input.default ?? input.value;
}
}
@@ -296,10 +272,36 @@ export default {
};
}
const runStrategyProps = ref({
title: "运行策略",
key: "strategy.runStrategy",
component: {
name: "a-select",
vModel: "value",
options: [
{ value: 0, label: "正常运行(证书申请任务请选择它)" },
{ value: 1, label: "成功后跳过(非证书任务请选择它)" }
]
},
helper: {
render: () => {
return (
<div>
<div>正常运行每次都运行证书任务需要每次都运行</div>
<div>成功后跳过在证书没变化时该任务成功一次之后跳过不重复部署</div>
<div>保持默认即可如果你想要再次测试部署可以临时设置为正常运行</div>
</div>
);
}
},
rules: [{ required: true, message: "此项必填" }]
});
return {
...useStepForm(),
labelCol: { span: 6 },
wrapperCol: { span: 16 }
wrapperCol: { span: 16 },
runStrategyProps
};
}
};

View File

@@ -1,49 +1,77 @@
<template>
<fs-page class="page-setting-email">
<template #header>
<div class="title">邮件设置</div>
<div class="title">
邮件设置
<span class="sub">设置邮件发送服务器</span>
</div>
</template>
<div class="email-form">
<a-form :model="formState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onFinish" @finish-failed="onFinishFailed">
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
<a-input v-model:value="formState.host" />
</a-form-item>
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
<a-input v-model:value="formState.port" />
</a-form-item>
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="formState.auth.user" />
</a-form-item>
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="formState.auth.pass" />
<div class="helper">如果是qq邮箱需要到qq邮箱的设置里面申请授权码作为密码</div>
</a-form-item>
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
<a-input v-model:value="formState.sender" />
</a-form-item>
<a-form-item label="是否ssl" name="secure">
<a-switch v-model:checked="formState.secure" />
<div class="helper">ssl和非ssl的smtp端口是不一样的注意修改端口</div>
</a-form-item>
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
<div>
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend">
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]">
<a-input v-model:value="testFormState.receiver" />
<div class="flex-o">
<div v-if="!formState.usePlus" class="email-form">
<a-form
:model="formState"
name="basic"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
autocomplete="off"
@finish="onFinish"
@finish-failed="onFinishFailed"
>
<a-form-item label="使用自定义邮件服务器"> </a-form-item>
<a-form-item label="SMTP域名" name="host" :rules="[{ required: true, message: '请输入smtp域名或ip' }]">
<a-input v-model:value="formState.host" />
</a-form-item>
<a-form-item label="SMTP端口" name="port" :rules="[{ required: true, message: '请输入smtp端口号' }]">
<a-input v-model:value="formState.port" />
</a-form-item>
<a-form-item label="用户名" :name="['auth', 'user']" :rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="formState.auth.user" />
</a-form-item>
<a-form-item label="密码" :name="['auth', 'pass']" :rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="formState.auth.pass" />
<div class="helper">如果是qq邮箱需要到qq邮箱的设置里面申请授权码作为密码</div>
</a-form-item>
<a-form-item label="发件邮箱" name="sender" :rules="[{ required: true, message: '请输入发件邮箱' }]">
<a-input v-model:value="formState.sender" />
</a-form-item>
<a-form-item label="是否ssl" name="secure">
<a-switch v-model:checked="formState.secure" />
<div class="helper">ssl和非ssl的smtp端口是不一样的注意修改端口</div>
</a-form-item>
<a-form-item label="忽略证书校验" name="tls.rejectUnauthorized">
<a-switch v-model:checked="formState.tls.rejectUnauthorized" />
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">测试</a-button>
<a-button type="primary" html-type="submit">保存</a-button>
</a-form-item>
</a-form>
</div>
<div class="email-form">
<a-form :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }">
<a-form-item label="使用官方邮件服务器">
<div class="flex-o">
<a-switch v-model:checked="formState.usePlus" :disabled="!userStore.isPlus" @change="onUsePlusChanged" />
<vip-button class="ml-5" mode="button"></vip-button>
</div>
<div class="helper">使用官方邮箱服务器直接发邮件免除繁琐的配置</div>
</a-form-item>
</a-form>
</div>
</div>
<div class="email-form">
<a-form :model="testFormState" name="basic" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" autocomplete="off" @finish="onTestSend">
<a-form-item label="测试收件邮箱" name="receiver" :rules="[{ required: true, message: '请输入测试收件邮箱' }]">
<a-input v-model:value="testFormState.receiver" />
<div class="helper">发送失败可以试试使用官方邮件服务器</div>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" :loading="testFormState.loading" html-type="submit">测试</a-button>
</a-form-item>
</a-form>
</div>
</fs-page>
</template>
@@ -55,6 +83,7 @@ import * as emailApi from "./api.email";
import { SettingKeys } from "./api";
import { notification } from "ant-design-vue";
import { useUserStore } from "/@/store/modules/user";
interface FormState {
host: string;
@@ -69,6 +98,7 @@ interface FormState {
rejectUnauthorized?: boolean;
};
sender: string;
usePlus: boolean;
}
const formState = reactive<Partial<FormState>>({
@@ -76,7 +106,8 @@ const formState = reactive<Partial<FormState>>({
user: "",
pass: ""
},
tls: {}
tls: {},
usePlus: false
});
async function load() {
@@ -99,6 +130,10 @@ const onFinishFailed = (errorInfo: any) => {
// console.log("Failed:", errorInfo);
};
async function onUsePlusChanged() {
await api.SettingsSave(SettingKeys.Email, formState);
}
interface TestFormState {
receiver: string;
loading: boolean;
@@ -118,6 +153,8 @@ async function onTestSend() {
testFormState.loading = false;
}
}
const userStore = useUserStore();
</script>
<style lang="less">
@@ -127,9 +164,9 @@ async function onTestSend() {
margin: 20px;
}
.helper{
padding:1px;
margin:0px;
.helper {
padding: 1px;
margin: 0px;
color: #999;
font-size: 10px;
}

View File

@@ -1,43 +0,0 @@
import { requestForMock } from "/src/api/service";
const request = requestForMock;
const apiPrefix = "/mock/AdvancedBigData";
export function GetList(query: any) {
return request({
url: apiPrefix + "/page",
method: "get",
data: query
});
}
export function AddObj(obj: any) {
return request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export function DelObj(id: any) {
return request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export function GetObj(id: any) {
return request({
url: apiPrefix + "/get",
method: "get",
params: { id }
});
}

View File

@@ -1,172 +0,0 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
if (form.id == null) {
form.id = row.id;
}
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
output: {},
crudOptions: {
//大量数据的crud配置
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
table: {
scroll: {
//启用横向滚动条,设置一个大于所有列宽之和的值,一般大于表格宽度
x: 2400
}
},
pagination: {
pageSize: 100
},
rowHandle: {
fixed: "right"
},
columns: {
id: {
title: "ID",
type: "number",
column: {
width: 50
},
form: {
show: false
}
},
text: {
title: "文本",
type: "text"
},
dict1: {
title: "字典1",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict1"
})
},
dict2: {
title: "字典2",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict2"
})
},
dict3: {
title: "字典3",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict3"
})
},
dict4: {
title: "字典4",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict4"
})
},
dict5: {
title: "字典5",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict5"
})
},
dict6: {
title: "字典6",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict6"
})
},
dict7: {
title: "字典7",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict7"
})
},
dict8: {
title: "字典8",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict8"
})
},
dict9: {
title: "字典9",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict9"
})
},
dict10: {
title: "字典10",
type: "dict-select",
dict: dict({
url: "/mock/dicts/ManyOpenStatusEnum?from=dict10"
})
},
text1: {
title: "文本",
type: "text"
},
text2: {
title: "文本",
type: "text"
},
text3: {
title: "文本",
type: "text"
},
text4: {
title: "文本",
type: "text"
},
text5: {
title: "文本",
type: "text"
},
text6: {
title: "文本",
type: "text"
},
text7: {
title: "文本",
type: "text"
},
text8: {
title: "文本",
type: "text"
},
text9: {
title: "文本",
type: "text"
},
text10: {
title: "文本",
type: "text"
}
}
}
};
}

View File

@@ -1,31 +0,0 @@
<template>
<fs-page>
<template #header>
<div class="title">大量数据</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud.js";
export default defineComponent({
name: "AdvancedBigData",
setup() {
const { crudBinding, crudRef, crudExpose, output } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef,
...output
};
}
});
</script>

View File

@@ -1,128 +0,0 @@
// @ts-ignore
import mockUtil from "/src/mock/base";
const options: any = {
name: "AdvancedBigData",
idGenerator: 0,
//此处copy多次模拟大量数据
copyTimes: 1000
};
const list = [
{
text: "测试文本",
dict1: "1",
dict2: "1",
dict3: "2",
dict4: "1",
dict5: "2",
dict6: "1",
dict7: "1",
dict8: "1",
text1: "测试文本1",
text2: "测试文本2",
text3: "测试文本3",
text4: "测试文本4",
text5: "测试文本5",
text6: "测试文本6",
text7: "测试文本7",
text8: "测试文本8",
dict9: "2",
dict10: "1",
dict11: "2",
dict12: "1"
},
{
text: "测试文本",
dict1: "1",
dict2: "1",
dict3: "2",
dict4: "1",
dict5: "2",
dict6: "1",
dict7: "1",
dict8: "1",
text1: "测试文本1",
text2: "测试文本2",
text3: "测试文本3",
text4: "测试文本4",
text5: "测试文本5",
text6: "测试文本6",
text7: "测试文本7",
text8: "测试文本8",
dict9: "2",
dict10: "1",
dict11: "2",
dict12: "1"
},
{
text: "测试文本",
dict1: "1",
dict2: "1",
dict3: "2",
dict4: "1",
dict5: "2",
dict6: "1",
dict7: "1",
dict8: "1",
text1: "测试文本1",
text2: "测试文本2",
text3: "测试文本3",
text4: "测试文本4",
text5: "测试文本5",
text6: "测试文本6",
text7: "测试文本7",
text8: "测试文本8",
dict9: "2",
dict10: "1",
dict11: "2",
dict12: "1"
},
{
text: "测试文本",
dict1: "1",
dict2: "1",
dict3: "2",
dict4: "1",
dict5: "2",
dict6: "1",
dict7: "1",
dict8: "1",
text1: "测试文本1",
text2: "测试文本2",
text3: "测试文本3",
text4: "测试文本4",
text5: "测试文本5",
text6: "测试文本6",
text7: "测试文本7",
text8: "测试文本8",
dict9: "2",
dict10: "1",
dict11: "2",
dict12: "1"
},
{
text: "测试文本",
dict1: "1",
dict2: "1",
dict3: "2",
dict4: "1",
dict5: "2",
dict6: "1",
dict7: "1",
dict8: "1",
text1: "测试文本1",
text2: "测试文本2",
text3: "测试文本3",
text4: "测试文本4",
text5: "测试文本5",
text6: "测试文本6",
text7: "测试文本7",
text8: "测试文本8",
dict9: "2",
dict10: "1",
dict11: "2",
dict12: "1"
}
];
options.list = list;
const mock = mockUtil.buildMock(options);
export default mock;

View File

@@ -1,48 +0,0 @@
import { requestForMock } from "/src/api/service";
const request = requestForMock;
const apiPrefix = "/mock/AdvancedFromBackend";
export function GetList(query: any) {
return request({
url: apiPrefix + "/page",
method: "get",
data: query
});
}
export function AddObj(obj: any) {
return request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export function DelObj(id: any) {
return request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export function GetObj(id: any) {
return request({
url: apiPrefix + "/get",
method: "get",
params: { id }
});
}
export function GetCrud() {
return request({
url: apiPrefix + "/crud",
method: "get"
});
}

View File

@@ -1,30 +0,0 @@
//此处演示从后台获取crudOptions配置字符串
export const crudOptions = `
({crudExpose,dict}) => {
return {
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 50
},
form: {
show: false
}
},
radio: {
title: "状态",
search: { show: true },
type: "dict-radio",
dict: dict({
url: "/mock/dicts/OpenStatusEnum?single"
})
}
}
}
}
`;

View File

@@ -1,51 +0,0 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes, dict } from "@fast-crud/fast-crud";
import { GetCrud } from "./api";
import _ from "lodash-es";
/**
* 异步创建options
* @param props
*/
export default async function (props: CreateCrudOptionsProps): Promise<CreateCrudOptionsRet> {
const { crudExpose } = props;
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
if (form.id == null) {
form.id = row.id;
}
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
const localCrudOptions = {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
}
};
// 上面是本地crudOptions
// 下面从后台获取crudOptions
const ret = await GetCrud();
// 编译
const crudBackend = eval(ret);
// 本示例返回的是一个方法字符串所以要先执行这个方法获取options
const remoteCrudOptions = crudBackend({ crudExpose, dict });
// 与本地options合并
const crudOptions = _.merge(localCrudOptions, remoteCrudOptions);
return {
crudOptions
};
}

View File

@@ -1,44 +0,0 @@
<template>
<fs-page>
<template #header>
<div class="title">CrudOptions从后台加载</div>
<div class="more">
<a target="_blank" href="http://fast-crud.docmirror.cn/api/use.html#usefsasync">文档</a>
</div>
</template>
<fs-crud v-if="crudBinding" ref="crudRef" v-bind="crudBinding" />
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, Ref } from "vue";
import { CrudBinding, useFsAsync } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
export default defineComponent({
name: "AdvancedFromBackend",
setup() {
// crud组件的ref
const crudRef: Ref = ref();
// crud 配置的ref
const crudBinding: Ref<CrudBinding> = ref();
const customValue: any = {}; //自定义变量传给createCrudOptions的额外参数
// 初始化crud配置
// 页面打开后获取列表数据
onMounted(async () => {
const customValue = {};
//异步初始化fscreateCrudOptions为异步方法
const { crudExpose, context } = await useFsAsync({ crudRef, crudBinding, createCrudOptions, context: customValue });
// 刷新数据
await crudExpose.doRefresh();
});
return {
crudBinding,
crudRef
};
}
});
</script>

View File

@@ -1,36 +0,0 @@
// @ts-ignore
import mockUtil from "/src/mock/base";
import { crudOptions } from "./crud-backend";
const options: any = {
name: "AdvancedFromBackend",
idGenerator: 0
};
const list = [
{
radio: "1"
},
{
radio: "2"
},
{
radio: "0"
}
];
options.list = list;
options.copyTimes = 1000;
const mock = mockUtil.buildMock(options);
mock.push({
path: "/AdvancedFromBackend/crud",
method: "get",
handle(req: any) {
return {
code: 0,
msg: "success",
data: crudOptions
};
}
});
export default mock;

View File

@@ -1,42 +0,0 @@
import { requestForMock } from "/src/api/service";
const request = requestForMock;
const apiPrefix = "/mock/AdvancedInDialog";
export function GetList(query: any) {
return request({
url: apiPrefix + "/page",
method: "get",
data: query
});
}
export function AddObj(obj: any) {
return request({
url: apiPrefix + "/add",
method: "post",
data: obj
});
}
export function UpdateObj(obj: any) {
return request({
url: apiPrefix + "/update",
method: "post",
data: obj
});
}
export function DelObj(id: any) {
return request({
url: apiPrefix + "/delete",
method: "post",
params: { id }
});
}
export function GetObj(id: any) {
return request({
url: apiPrefix + "/get",
method: "get",
params: { id }
});
}

View File

@@ -1,115 +0,0 @@
import * as api from "./api";
import {
AddReq,
CreateCrudOptionsProps,
CreateCrudOptionsRet,
DelReq,
EditReq,
UserPageQuery,
UserPageRes,
dict,
utils
} from "@fast-crud/fast-crud";
import { SearchOutlined } from "@ant-design/icons-vue";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
if (form.id == null) {
form.id = row.id;
}
return await api.UpdateObj(form);
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
return await api.AddObj(form);
};
return {
crudOptions: {
request: {
pageRequest,
addRequest,
editRequest,
delRequest
},
columns: {
name: {
title: "姓名",
type: "text", //虽然不写也能正确显示组件,但不建议省略它
search: { show: true },
form: {
component: {
maxlength: 20
}
}
},
search: {
title: "搜索",
type: "text",
form: {
component: {
addonAfter: "后置",
suffix: "suffix",
children: {
addonBefore() {
return <SearchOutlined />;
}
}
}
}
},
password: {
title: "密码",
type: "password",
column: {
//一般密码不显示在列里面
show: false
}
},
intro: {
title: "简介",
type: "textarea",
form: {
component: { showWordLimit: true, maxlength: 200 }
},
column: {
ellipsis: true
}
},
render: {
title: "复杂输入(render)",
type: "text",
form: {
title: "复杂输入",
component: {
render(context: any) {
utils.logger.info("context scope", context);
return (
<a-input-group compact>
<a-input placeholder={"render1 input"} style="width: 50%" v-model={[context.form.render, "value"]} />
<a-input placeholder={"render2 input"} style="width: 50%" v-model={[context.form.render2, "value"]} />
</a-input-group>
);
}
}
}
},
render2: {
title: "我的值是由复杂输入列输入的",
type: "text",
column: {
width: "300px"
},
form: {
show: false
}
}
}
}
};
}

View File

@@ -1,28 +0,0 @@
<template>
<fs-page>
<fs-crud ref="crudRef" v-bind="crudBinding" />
</fs-page>
</template>
<script lang="ts">
import { defineComponent, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
export default defineComponent({
name: "FsInDialog",
setup() {
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions });
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
return {
crudBinding,
crudRef
};
}
});
</script>

View File

@@ -1,41 +0,0 @@
// @ts-ignore
import mockUtil from "/src/mock/base";
const options: any = {
name: "AdvancedInDialog",
idGenerator: 0
};
const list = [
{
name: "王小虎",
date: "2016-05-02",
status: "0",
province: "1",
avatar: "https://alicdn.antdv.com/vue.png",
show: true,
city: "sz",
address: "123123",
zip: "518000",
intro: "王小虎是element-plus的table示例出现的名字"
},
{
name: "张三",
date: "2016-05-04",
status: "1",
province: "2"
},
{
name: "李四",
date: 2232433534511,
status: "1",
province: "0"
},
{
name: "王五",
date: "2016-05-03",
status: "2",
province: "wh,gz"
}
];
options.list = list;
const mock = mockUtil.buildMock(options);
export default mock;

View File

@@ -1,37 +0,0 @@
<template>
<fs-page>
<template #header>
<div class="title">对话框中显示crud</div>
</template>
<div style="padding: 50px">
<a-button type="primary" @click="openDialog">打开对话框</a-button>
</div>
<a-modal v-model:open="dialogShow" width="80%" title="fs-crud in dialog">
<div style="height: 400px; position: relative">
<!-- 在此处显示fs-crud页面 -->
<fs-in-dialog></fs-in-dialog>
</div>
</a-modal>
</fs-page>
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
//将fs-crud做成的页面在此处引入
import FsInDialog from "./crud/index.vue";
export default defineComponent({
name: "InDialog",
components: { FsInDialog },
setup() {
const dialogShow = ref(false);
function openDialog() {
dialogShow.value = true;
}
return {
dialogShow,
openDialog
};
}
});
</script>

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