mirror of
https://github.com/certd/certd.git
synced 2026-04-14 12:30:54 +08:00
Compare commits
65 Commits
client_syn
...
v0.1.11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2942d39dfe | ||
|
|
df65b0509e | ||
|
|
fbde35483b | ||
|
|
7370f8b83b | ||
|
|
8b0ca1da2e | ||
|
|
466f2b1a02 | ||
|
|
f1d6cce88c | ||
|
|
ad7ababb4c | ||
|
|
72fa623674 | ||
|
|
e5d117c134 | ||
|
|
f8944a1331 | ||
|
|
e850855154 | ||
|
|
06eacee90c | ||
|
|
b1e100982e | ||
|
|
8f30158b00 | ||
|
|
576f7db978 | ||
|
|
137e043dfe | ||
|
|
15467fc233 | ||
|
|
eec0fcdcf1 | ||
|
|
a41dee015e | ||
|
|
d1a74713ef | ||
|
|
813e9e71d7 | ||
|
|
3d08dce26e | ||
|
|
4739d75f4a | ||
|
|
30cd62664b | ||
|
|
ae6b0fb111 | ||
|
|
9ec48f6ab8 | ||
|
|
f68565f444 | ||
|
|
ce5aae3795 | ||
|
|
bd00c09da0 | ||
|
|
44326c3abe | ||
|
|
2f3db7d982 | ||
|
|
9e4e3044b4 | ||
|
|
23bf0d07f7 | ||
|
|
6e61f8bcfb | ||
|
|
c7b28feb07 | ||
|
|
624eade9f2 | ||
|
|
d727a77289 | ||
|
|
c0c2cb328c | ||
|
|
8cc80deff8 | ||
|
|
5312c11472 | ||
|
|
f07ce6f47d | ||
|
|
259e797ea5 | ||
|
|
d77addd2dc | ||
|
|
c2420edb5a | ||
|
|
e1396bb107 | ||
|
|
b0def03790 | ||
|
|
9f371df372 | ||
|
|
986fd4b010 | ||
|
|
4cd7b02cb7 | ||
|
|
67bff28255 | ||
|
|
43e90503ca | ||
|
|
25dae3d1ec | ||
|
|
ec81572670 | ||
|
|
71f6d5769a | ||
|
|
49b96592a0 | ||
|
|
af0875ac4c | ||
|
|
48192b9002 | ||
|
|
ca289d4604 | ||
|
|
6529fd2fdb | ||
|
|
e84f5e8b0a | ||
|
|
df9f561fd3 | ||
|
|
06603759fd | ||
|
|
a4bd29e6bf | ||
|
|
458486dd6b |
29
.gitignore
vendored
29
.gitignore
vendored
@@ -1,32 +1,11 @@
|
|||||||
# IntelliJ project files
|
# IntelliJ project files
|
||||||
.vscode/
|
|
||||||
node_modules/
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
yarn.lock
|
|
||||||
package-lock.json
|
|
||||||
/.idea/
|
|
||||||
*/**/dist
|
|
||||||
*/**/pnpm-lock.yaml
|
|
||||||
*/**/stats.html
|
|
||||||
.idea
|
.idea
|
||||||
*.iml
|
*.iml
|
||||||
out
|
out
|
||||||
gen
|
gen
|
||||||
|
node_modules/
|
||||||
/test/*.private.*
|
/test/*.private.*
|
||||||
|
/other/node-acme-client/.idea/
|
||||||
/*.log
|
/*.log
|
||||||
|
/other/certd-run
|
||||||
/packages/ui/*/.idea
|
/other/node-acme-client
|
||||||
|
|
||||||
/packages/ui/*/node_modules
|
|
||||||
|
|
||||||
/packages/*/node_modules
|
|
||||||
/packages/ui/certd-server/tmp/
|
|
||||||
/packages/ui/certd-ui/dist/
|
|
||||||
/other
|
|
||||||
/dev-sidecar-test
|
|
||||||
/packages/core/certd/yarn.lock
|
|
||||||
/packages/test
|
|
||||||
/test/own
|
|
||||||
/pnpm-lock.yaml
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2021 fast-crud
|
Copyright (c) 2021 Greper
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
196
README.md
Normal file
196
README.md
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
# CertD
|
||||||
|
|
||||||
|
CertD 是一个帮助你全自动申请和部署SSL证书的工具。
|
||||||
|
后缀D取自linux守护进程的命名风格,意为证书守护进程。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
本项目不仅支持证书申请过程自动化,还可以自动化部署证书,让你的证书永不过期。
|
||||||
|
|
||||||
|
* 全自动申请证书
|
||||||
|
* 全自动部署证书(目前支持服务器上传部署、阿里云、腾讯云等)
|
||||||
|
* 可与CI/DI工具结合使用
|
||||||
|
|
||||||
|
## 免费证书申请说明
|
||||||
|
* 本项目ssl证书提供商为letencrypt
|
||||||
|
* 申请过程遵循acme协议
|
||||||
|
* 需要验证域名所有权,一般有两种方式(目前本项目仅支持dns-01)
|
||||||
|
* http-01: 在网站根目录下放置一份txt文件
|
||||||
|
* dns-01: 需要给域名添加txt解析记录,泛域名只能用这种方式
|
||||||
|
* 证书续期:
|
||||||
|
* 实际上acme并没有续期概念。
|
||||||
|
* 我们所说的续期,其实就是按照全套流程重新申请一份新证书。
|
||||||
|
* 免费证书过期时间90天,以后可能还会缩短,所以自动化部署必不可少
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
本案例演示,如何配置自动申请证书,并部署到阿里云CDN,然后快要到期前自动更新证书并重新部署
|
||||||
|
|
||||||
|
1. 环境准备
|
||||||
|
安装[nodejs](https://nodejs.org/zh-cn/)
|
||||||
|
|
||||||
|
2. 创建任务项目
|
||||||
|
```
|
||||||
|
mkdir certd-run # 项目名称可以任意命名
|
||||||
|
cd certd-run -y
|
||||||
|
npm install @certd/executor -s --production
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 创建index.js
|
||||||
|
|
||||||
|
参数配置分几个部分
|
||||||
|
args: 运行时参数
|
||||||
|
accessProviders: 授权提供者,提供dns验证与部署任务的授权
|
||||||
|
cert: 证书申请的配置
|
||||||
|
deploy: 证书部署流程
|
||||||
|
|
||||||
|
```js
|
||||||
|
import { Executor } from '@certd/executor'
|
||||||
|
const options = {
|
||||||
|
args: { // 运行时参数
|
||||||
|
forceDeploy: true,
|
||||||
|
},
|
||||||
|
accessProviders: { //授权提供者
|
||||||
|
aliyun: { // 阿里云accessKey,用于dns验证和上传证书到阿里云,并部署到cdn
|
||||||
|
providerType: 'aliyun',
|
||||||
|
accessKeyId: 'Your accessKeyId',
|
||||||
|
accessKeySecret: 'Your accessKeySecret'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cert: { //免费证书申请配置
|
||||||
|
domains: [ //可以在一张证书上绑定多个域名(前提是他们的验证方式要一样,目前仅支持dns验证)
|
||||||
|
'*.yourdomain.com',
|
||||||
|
'*.test.yourdomain.com',
|
||||||
|
'yourdomain.com'
|
||||||
|
],
|
||||||
|
email: 'Your email',
|
||||||
|
dnsProvider: 'aliyun', //上方accessProviders里面配置的
|
||||||
|
csrInfo: { //证书csr信息
|
||||||
|
country: 'CN',
|
||||||
|
state: 'GuangDong',
|
||||||
|
locality: 'ShengZhen',
|
||||||
|
organization: 'Your company Org.',
|
||||||
|
organizationUnit: 'IT Department',
|
||||||
|
emailAddress: 'Your email'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deploy: [ //部署流程配置,数组,可以配置多条流程
|
||||||
|
{
|
||||||
|
deployName: '流程1-部署到阿里云CDN',
|
||||||
|
tasks: [ //流程任务,一个流程下可以包含多个部署任务,并且将按顺序执行
|
||||||
|
{ //任务1
|
||||||
|
taskName: '上传到阿里云', //任务名称
|
||||||
|
type: 'uploadCertToAliyun', //任务插件名称
|
||||||
|
props: { //任务插件参数
|
||||||
|
accessProvider: 'aliyun'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ // 任务2
|
||||||
|
taskName: '部署证书到CDN',
|
||||||
|
type: 'deployCertToAliyunCDN', //任务插件名称
|
||||||
|
props:{
|
||||||
|
domainName: 'your cdn domain 全称', //cdn域名全称
|
||||||
|
certName: 'certd自动部署',//证书名称前缀
|
||||||
|
accessProvider: 'aliyun'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const executor = new Executor()
|
||||||
|
await executor.run(options)
|
||||||
|
```
|
||||||
|
|
||||||
|
4. 运行
|
||||||
|
```
|
||||||
|
node index.js
|
||||||
|
```
|
||||||
|
5. 执行效果
|
||||||
|
生成的证书默认会存储在 `${home}/.certd/${email}/certs/${domain}/current`目录下
|
||||||
|
```
|
||||||
|
[2021-01-08T16:15:04.681] [INFO] certd - 任务完成
|
||||||
|
[2021-01-08T16:15:04.681] [INFO] certd - ---------------------------任务结果总览--------------------------
|
||||||
|
[2021-01-08T16:15:04.682] [INFO] certd - 【更新证书】--------------------------------------- [success]
|
||||||
|
证书申请成功
|
||||||
|
[2021-01-08T16:15:04.682] [INFO] certd - 【流程1-部署到阿里云CDN】---------------------------- [success] 执行成功
|
||||||
|
[2021-01-08T16:15:04.682] [INFO] certd - └【上传到阿里云】-------------------------------- [success] 执行成功
|
||||||
|
[2021-01-08T16:15:04.682] [INFO] certd - └【部署证书到CDN】------------------------------- [success] 执行成功
|
||||||
|
```
|
||||||
|
6. 证书续期
|
||||||
|
实际上没有证书续期的概念,只有重新生成一份新的证书,然后重新部署证书
|
||||||
|
所以每天定时运行即可,当证书过期日前20天时,会重新申请新的证书,然后执行部署任务。
|
||||||
|
|
||||||
|
7. 其他说明
|
||||||
|
证书的部署任务执行后会记录执行结果,已经成功过的不会重复执行
|
||||||
|
所以当你临时需要将证书部署到其他地方时,直接追加部署任务,然后再次运行即可
|
||||||
|
|
||||||
|
## CI/DI集成与自动续期重新部署
|
||||||
|
集成前,将以上代码提交到内网git仓库,或者私有git仓库(由于包含敏感信息,不要提交到公开git仓库)
|
||||||
|
|
||||||
|
### jenkins任务
|
||||||
|
1. 创建任务
|
||||||
|
选择构建自由风格的任务
|
||||||
|
|
||||||
|
2. 配置git
|
||||||
|
配置cert-run的git地址
|
||||||
|
|
||||||
|
3. 构建触发器
|
||||||
|
配置 `H 3 * * *` ,每天凌晨3点-4点执行一次
|
||||||
|
|
||||||
|
4. 构建环境
|
||||||
|
勾选 `Provide Node & npm bin/ folder to PATH`,提供nodejs运行环境
|
||||||
|
如果没有此选项,需要jenkins安装`nodejs`插件
|
||||||
|
|
||||||
|
5. 构建
|
||||||
|
执行shell
|
||||||
|
```
|
||||||
|
npm install --production #执行过一次之后,就可以注释掉,加快执行速度
|
||||||
|
npm run post
|
||||||
|
```
|
||||||
|
6. 构建后操作
|
||||||
|
邮件通知
|
||||||
|
配置你的邮箱地址,可以在执行失败时收到邮件通知。
|
||||||
|
|
||||||
|
|
||||||
|
## API
|
||||||
|
先列个提纲,待完善
|
||||||
|
|
||||||
|
参数示例参考:https://gitee.com/certd/certd/blob/master/test/options.js
|
||||||
|
|
||||||
|
### 授权提供者
|
||||||
|
用于dns验证接口调用
|
||||||
|
#### aliyun
|
||||||
|
|
||||||
|
#### dnspod
|
||||||
|
|
||||||
|
### deploy插件
|
||||||
|
部署任务插件
|
||||||
|
#### 阿里云
|
||||||
|
##### 上传到阿里云
|
||||||
|
type = uploadCertToAliyun
|
||||||
|
##### 部署到阿里云DNS
|
||||||
|
type = deployCertToAliyunCDN
|
||||||
|
|
||||||
|
##### 部署到阿里云CLB
|
||||||
|
type = deployCertToAliyunCLB
|
||||||
|
|
||||||
|
#### 腾讯云
|
||||||
|
##### 上传到腾讯云
|
||||||
|
type = uploadCertToTencent
|
||||||
|
|
||||||
|
##### 部署到腾讯云DNS
|
||||||
|
type = deployCertToTencentDNS
|
||||||
|
|
||||||
|
##### 部署到腾讯云CLB
|
||||||
|
type = deployCertToTencentCLB
|
||||||
|
|
||||||
|
##### 部署到腾讯云TKE-ingress
|
||||||
|
type = deployCertToTencentTKEIngress
|
||||||
|
|
||||||
|
|
||||||
|
### 更多部署插件
|
||||||
|
等你来提需求
|
||||||
6
lerna.json
Normal file
6
lerna.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"packages": [
|
||||||
|
"packages/*"
|
||||||
|
],
|
||||||
|
"version": "0.1.11"
|
||||||
|
}
|
||||||
14
package.json
Normal file
14
package.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "root",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"lerna": "^3.18.4"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash-es": "^4.17.20"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/api/.eslintrc
Normal file
14
packages/api/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "standard",
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.js", "*.spec.js"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2468
packages/api/package-lock.json
generated
Normal file
2468
packages/api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
packages/api/package.json
Normal file
25
packages/api/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "@certd/api",
|
||||||
|
"version": "0.1.11",
|
||||||
|
"description": "",
|
||||||
|
"main": "./src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"author": "Greper",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.1",
|
||||||
|
"dayjs": "^1.9.7",
|
||||||
|
"lodash-es": "^4.17.20",
|
||||||
|
"log4js": "^6.3.0",
|
||||||
|
"qs": "^6.9.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^7.15.0",
|
||||||
|
"eslint-config-standard": "^16.0.2",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"mocha": "^8.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/api/src/dns-provider/index.js
Normal file
34
packages/api/src/dns-provider/index.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import _ from 'lodash-es'
|
||||||
|
import logger from '../utils/util.log.js'
|
||||||
|
export class AbstractDnsProvider {
|
||||||
|
constructor () {
|
||||||
|
this.logger = logger
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRecord ({ fullRecord, type, value }) {
|
||||||
|
throw new Error('请实现 createRecord 方法')
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRecord ({ fullRecord, type, value, record }) {
|
||||||
|
throw new Error('请实现 removeRecord 方法')
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDomainList () {
|
||||||
|
throw new Error('请实现 getDomainList 方法')
|
||||||
|
}
|
||||||
|
|
||||||
|
async matchDomain (dnsRecord, domainPropName) {
|
||||||
|
const list = await this.getDomainList()
|
||||||
|
let domain = null
|
||||||
|
for (const item of list) {
|
||||||
|
if (_.endsWith(dnsRecord, item[domainPropName])) {
|
||||||
|
domain = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error('找不到域名,请检查域名是否正确:' + dnsRecord)
|
||||||
|
}
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
}
|
||||||
4
packages/api/src/index.js
Normal file
4
packages/api/src/index.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export { AbstractDnsProvider } from './dns-provider/index.js'
|
||||||
|
export { Store } from './store/store.js'
|
||||||
|
export { util } from './utils/index.js'
|
||||||
|
export { AbstractPlugin } from './plugin/index.js'
|
||||||
72
packages/api/src/plugin/index.js
Normal file
72
packages/api/src/plugin/index.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import logger from '../utils/util.log.js'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import Sleep from '../utils/util.sleep.js'
|
||||||
|
export class AbstractPlugin {
|
||||||
|
constructor ({ accessProviders }) {
|
||||||
|
this.logger = logger
|
||||||
|
this.accessProviders = accessProviders
|
||||||
|
}
|
||||||
|
|
||||||
|
appendTimeSuffix (name) {
|
||||||
|
if (name == null) {
|
||||||
|
name = 'certd'
|
||||||
|
}
|
||||||
|
return name + '-' + dayjs().format('YYYYMMDD-HHmmss')
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeFromContextFile (options = {}) {
|
||||||
|
const { contextPath } = options
|
||||||
|
const contextJson = fs.readFileSync(contextPath)
|
||||||
|
const context = JSON.parse(contextJson)
|
||||||
|
options.context = context
|
||||||
|
await this.doExecute(options)
|
||||||
|
fs.writeFileSync(JSON.stringify(context))
|
||||||
|
}
|
||||||
|
|
||||||
|
async doExecute (options) {
|
||||||
|
try {
|
||||||
|
return await this.execute(options)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('插件执行出错:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行
|
||||||
|
* @param options
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async execute (options) {
|
||||||
|
console.error('请实现此方法,context:', options.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRollback (options) {
|
||||||
|
try {
|
||||||
|
return await this.rollback(options)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('插件rollback出错:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回退,如有必要
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
async rollback (options) {
|
||||||
|
console.error('请实现此方法,rollback:', options.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
|
||||||
|
if (typeof accessProvider === 'string' && accessProviders) {
|
||||||
|
accessProvider = accessProviders[accessProvider]
|
||||||
|
}
|
||||||
|
return accessProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
async sleep (time) {
|
||||||
|
await Sleep(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/api/src/store/store.js
Normal file
33
packages/api/src/store/store.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export class Store {
|
||||||
|
set (key, value) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
get (key) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
buildKey (...keyItem) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
linkExists (linkPath) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
link (targetPath, linkPath) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink (linkPath) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全路径
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
getActualKey (key) {
|
||||||
|
// return 前缀+key
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/api/src/utils/index.js
Normal file
7
packages/api/src/utils/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import logger from './util.log.js'
|
||||||
|
import path from './util.path.js'
|
||||||
|
import { request } from './util.request.js'
|
||||||
|
import sleep from './util.sleep.js'
|
||||||
|
export const util = {
|
||||||
|
logger, path, request, sleep
|
||||||
|
}
|
||||||
7
packages/api/src/utils/util.log.js
Normal file
7
packages/api/src/utils/util.log.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import log4js from 'log4js'
|
||||||
|
log4js.configure({
|
||||||
|
appenders: { std: { type: 'stdout' } },
|
||||||
|
categories: { default: { appenders: ['std'], level: 'info' } }
|
||||||
|
})
|
||||||
|
const logger = log4js.getLogger('certd')
|
||||||
|
export default logger
|
||||||
9
packages/api/src/utils/util.path.js
Normal file
9
packages/api/src/utils/util.path.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
function getUserBasePath () {
|
||||||
|
const userHome = process.env.USERPROFILE || process.env.HOME
|
||||||
|
return path.resolve(userHome, './.certd')
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
getUserBasePath
|
||||||
|
}
|
||||||
57
packages/api/src/utils/util.request.js
Normal file
57
packages/api/src/utils/util.request.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import qs from 'qs'
|
||||||
|
import logger from './util.log.js'
|
||||||
|
/**
|
||||||
|
* @description 创建请求实例
|
||||||
|
*/
|
||||||
|
function createService () {
|
||||||
|
// 创建一个 axios 实例
|
||||||
|
const service = axios.create()
|
||||||
|
// 请求拦截
|
||||||
|
service.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
if (config.formData) {
|
||||||
|
config.data = qs.stringify(config.formData, {
|
||||||
|
arrayFormat: 'indices',
|
||||||
|
allowDots: true
|
||||||
|
}) // 序列化请求参数
|
||||||
|
delete config.formData
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// 发送失败
|
||||||
|
logger.error(error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// 响应拦截
|
||||||
|
service.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
logger.info('http response:', JSON.stringify(response.data))
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// const status = _.get(error, 'response.status')
|
||||||
|
// switch (status) {
|
||||||
|
// case 400: error.message = '请求错误'; break
|
||||||
|
// case 401: error.message = '未授权,请登录'; break
|
||||||
|
// case 403: error.message = '拒绝访问'; break
|
||||||
|
// case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
|
||||||
|
// case 408: error.message = '请求超时'; break
|
||||||
|
// case 500: error.message = '服务器内部错误'; break
|
||||||
|
// case 501: error.message = '服务未实现'; break
|
||||||
|
// case 502: error.message = '网关错误'; break
|
||||||
|
// case 503: error.message = '服务不可用'; break
|
||||||
|
// case 504: error.message = '网关超时'; break
|
||||||
|
// case 505: error.message = 'HTTP版本不受支持'; break
|
||||||
|
// default: break
|
||||||
|
// }
|
||||||
|
logger.error('请求出错:', error.response.config.url, error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
export const request = createService()
|
||||||
7
packages/api/src/utils/util.sleep.js
Normal file
7
packages/api/src/utils/util.sleep.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function (timeout) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve()
|
||||||
|
}, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
14
packages/certd/.eslintrc
Normal file
14
packages/certd/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "standard",
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.js", "*.spec.js"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2944
packages/certd/package-lock.json
generated
Normal file
2944
packages/certd/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
packages/certd/package.json
Normal file
29
packages/certd/package.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "@certd/certd",
|
||||||
|
"version": "0.1.11",
|
||||||
|
"description": "",
|
||||||
|
"main": "./src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"author": "Greper",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@certd/acme-client": "^0.1.6",
|
||||||
|
"@certd/api": "^0.1.11",
|
||||||
|
"@certd/providers": "^0.1.11",
|
||||||
|
"dayjs": "^1.9.7",
|
||||||
|
"lodash-es": "^4.17.20",
|
||||||
|
"node-forge": "^0.10.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^7.15.0",
|
||||||
|
"eslint-config-standard": "^16.0.2",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"mocha": "^8.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
199
packages/certd/src/acme.js
Normal file
199
packages/certd/src/acme.js
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import acme from '@certd/acme-client'
|
||||||
|
import _ from 'lodash-es'
|
||||||
|
import { util } from '@certd/api'
|
||||||
|
const logger = util.logger
|
||||||
|
export class AcmeService {
|
||||||
|
constructor (store) {
|
||||||
|
this.store = store
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccountConfig (email) {
|
||||||
|
let conf = this.store.get(this.buildAccountPath(email))
|
||||||
|
if (conf == null) {
|
||||||
|
conf = {}
|
||||||
|
} else {
|
||||||
|
conf = JSON.parse(conf)
|
||||||
|
}
|
||||||
|
return conf
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAccountPath (email) {
|
||||||
|
return this.store.buildKey(email, 'account.json')
|
||||||
|
}
|
||||||
|
|
||||||
|
saveAccountConfig (email, conf) {
|
||||||
|
this.store.set(this.buildAccountPath(email), JSON.stringify(conf))
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAcmeClient (email, isTest) {
|
||||||
|
const conf = await this.getAccountConfig(email)
|
||||||
|
if (conf.key == null) {
|
||||||
|
conf.key = await this.createNewKey()
|
||||||
|
this.saveAccountConfig(email, conf)
|
||||||
|
}
|
||||||
|
if (isTest == null) {
|
||||||
|
isTest = process.env.CERTD_MODE === 'test'
|
||||||
|
}
|
||||||
|
const client = new acme.Client({
|
||||||
|
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
|
||||||
|
accountKey: conf.key,
|
||||||
|
accountUrl: conf.accountUrl,
|
||||||
|
backoffAttempts: 20,
|
||||||
|
backoffMin: 5000,
|
||||||
|
backoffMax: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
if (conf.accountUrl == null) {
|
||||||
|
const accountPayload = { termsOfServiceAgreed: true, contact: [`mailto:${email}`] }
|
||||||
|
await client.createAccount(accountPayload)
|
||||||
|
conf.accountUrl = client.getAccountUrl()
|
||||||
|
this.saveAccountConfig(email, conf)
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewKey () {
|
||||||
|
const key = await acme.forge.createPrivateKey()
|
||||||
|
return key.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
async challengeCreateFn (authz, challenge, keyAuthorization, dnsProvider) {
|
||||||
|
logger.info('Triggered challengeCreateFn()')
|
||||||
|
|
||||||
|
/* http-01 */
|
||||||
|
if (challenge.type === 'http-01') {
|
||||||
|
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
|
||||||
|
const fileContents = keyAuthorization
|
||||||
|
|
||||||
|
logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`)
|
||||||
|
|
||||||
|
/* Replace this */
|
||||||
|
logger.info(`Would write "${fileContents}" to path "${filePath}"`)
|
||||||
|
// await fs.writeFileAsync(filePath, fileContents);
|
||||||
|
} else if (challenge.type === 'dns-01') {
|
||||||
|
/* dns-01 */
|
||||||
|
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
|
||||||
|
const recordValue = keyAuthorization
|
||||||
|
|
||||||
|
logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`)
|
||||||
|
|
||||||
|
/* Replace this */
|
||||||
|
logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`)
|
||||||
|
|
||||||
|
return await dnsProvider.createRecord({
|
||||||
|
fullRecord: dnsRecord,
|
||||||
|
type: 'TXT',
|
||||||
|
value: recordValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function used to remove an ACME challenge response
|
||||||
|
*
|
||||||
|
* @param {object} authz Authorization object
|
||||||
|
* @param {object} challenge Selected challenge
|
||||||
|
* @param {string} keyAuthorization Authorization key
|
||||||
|
* @param recordItem challengeCreateFn create record item
|
||||||
|
* @param dnsProvider dnsProvider
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
|
||||||
|
async challengeRemoveFn (authz, challenge, keyAuthorization, recordItem, dnsProvider) {
|
||||||
|
logger.info('Triggered challengeRemoveFn()')
|
||||||
|
|
||||||
|
/* http-01 */
|
||||||
|
if (challenge.type === 'http-01') {
|
||||||
|
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`
|
||||||
|
|
||||||
|
logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`)
|
||||||
|
|
||||||
|
/* Replace this */
|
||||||
|
logger.info(`Would remove file on path "${filePath}"`)
|
||||||
|
// await fs.unlinkAsync(filePath);
|
||||||
|
} else if (challenge.type === 'dns-01') {
|
||||||
|
const dnsRecord = `_acme-challenge.${authz.identifier.value}`
|
||||||
|
const recordValue = keyAuthorization
|
||||||
|
|
||||||
|
logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`)
|
||||||
|
|
||||||
|
/* Replace this */
|
||||||
|
logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`)
|
||||||
|
await dnsProvider.removeRecord({
|
||||||
|
fullRecord: dnsRecord,
|
||||||
|
type: 'TXT',
|
||||||
|
value: keyAuthorization,
|
||||||
|
record: recordItem
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async order ({ email, domains, dnsProvider, dnsProviderCreator, csrInfo, isTest }) {
|
||||||
|
const client = await this.getAcmeClient(email, isTest)
|
||||||
|
|
||||||
|
let accountUrl
|
||||||
|
try {
|
||||||
|
accountUrl = client.getAccountUrl()
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create CSR */
|
||||||
|
const { commonName, altNames } = this.buildCommonNameByDomains(domains)
|
||||||
|
|
||||||
|
const [key, csr] = await acme.forge.createCsr({
|
||||||
|
commonName,
|
||||||
|
...csrInfo,
|
||||||
|
altNames
|
||||||
|
})
|
||||||
|
if (dnsProvider == null && dnsProviderCreator) {
|
||||||
|
dnsProvider = await dnsProviderCreator()
|
||||||
|
}
|
||||||
|
if (dnsProvider == null) {
|
||||||
|
throw new Error('dnsProvider 不能为空')
|
||||||
|
}
|
||||||
|
/* 自动申请证书 */
|
||||||
|
const crt = await client.auto({
|
||||||
|
csr,
|
||||||
|
email: email,
|
||||||
|
termsOfServiceAgreed: true,
|
||||||
|
challengePriority: ['dns-01'],
|
||||||
|
challengeCreateFn: async (authz, challenge, keyAuthorization) => {
|
||||||
|
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider)
|
||||||
|
},
|
||||||
|
challengeRemoveFn: async (authz, challenge, keyAuthorization, recordItem) => {
|
||||||
|
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存账号url
|
||||||
|
if (!accountUrl) {
|
||||||
|
try {
|
||||||
|
accountUrl = client.getAccountUrl()
|
||||||
|
this.setAccountUrl(email, accountUrl)
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('保存accountUrl出错', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Done */
|
||||||
|
logger.debug(`CSR:\n${csr.toString()}`)
|
||||||
|
logger.debug(`Certificate:\n${crt.toString()}`)
|
||||||
|
logger.info('证书申请成功')
|
||||||
|
return { key, crt, csr }
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCommonNameByDomains (domains) {
|
||||||
|
if (typeof domains === 'string') {
|
||||||
|
domains = domains.split(',')
|
||||||
|
}
|
||||||
|
if (domains.length === 0) {
|
||||||
|
throw new Error('domain can not be empty')
|
||||||
|
}
|
||||||
|
const ret = {
|
||||||
|
commonName: domains[0]
|
||||||
|
}
|
||||||
|
if (domains.length > 1) {
|
||||||
|
ret.altNames = _.slice(domains, 1)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
}
|
||||||
148
packages/certd/src/index.js
Normal file
148
packages/certd/src/index.js
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
import { util, Store } from '@certd/api'
|
||||||
|
import { AcmeService } from './acme.js'
|
||||||
|
import { FileStore } from './store/file-store.js'
|
||||||
|
import { CertStore } from './store/cert-store.js'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import forge from 'node-forge'
|
||||||
|
import DefaultProviders from '@certd/providers'
|
||||||
|
import _ from 'lodash-es'
|
||||||
|
const logger = util.logger
|
||||||
|
|
||||||
|
const AccessProviderClasses = {}
|
||||||
|
function install (providerClass) {
|
||||||
|
AccessProviderClasses[providerClass.name()] = providerClass
|
||||||
|
}
|
||||||
|
logger.info('use')
|
||||||
|
_.forEach(DefaultProviders, item => {
|
||||||
|
logger.info('use:', item.name())
|
||||||
|
install(item)
|
||||||
|
})
|
||||||
|
export class Certd {
|
||||||
|
static use (providerClass) {
|
||||||
|
install(providerClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (options) {
|
||||||
|
this.options = options
|
||||||
|
this.email = options.cert.email
|
||||||
|
this.domains = options.cert.domains
|
||||||
|
|
||||||
|
if (!(options.store instanceof Store)) {
|
||||||
|
this.store = new FileStore(options.store || {})
|
||||||
|
}
|
||||||
|
this.certStore = new CertStore({
|
||||||
|
store: this.store,
|
||||||
|
email: options.cert.email,
|
||||||
|
domains: this.domains
|
||||||
|
})
|
||||||
|
this.acme = new AcmeService(this.store)
|
||||||
|
}
|
||||||
|
|
||||||
|
async certApply () {
|
||||||
|
let oldCert
|
||||||
|
try {
|
||||||
|
oldCert = await this.readCurrentCert()
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn('读取cert失败:', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldCert == null) {
|
||||||
|
logger.info('还未申请过,准备申请新证书')
|
||||||
|
} else {
|
||||||
|
const ret = this.isWillExpire(oldCert.expires, this.options.cert.renewDays)
|
||||||
|
if (!ret.isWillExpire) {
|
||||||
|
logger.info('证书还未过期:', oldCert.expires, ',剩余', ret.leftDays, '天')
|
||||||
|
if (this.options.args.forceCert) {
|
||||||
|
logger.info('准备强制更新证书')
|
||||||
|
} else {
|
||||||
|
logger.info('暂不更新证书')
|
||||||
|
|
||||||
|
oldCert.isNew = false
|
||||||
|
return oldCert
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('即将过期,准备更新证书')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行证书申请步骤
|
||||||
|
return await this.doCertApply()
|
||||||
|
}
|
||||||
|
|
||||||
|
async doCertApply () {
|
||||||
|
const options = this.options
|
||||||
|
const dnsProvider = this.createDnsProvider(options)
|
||||||
|
const cert = await this.acme.order({
|
||||||
|
email: options.cert.email,
|
||||||
|
domains: options.cert.domains,
|
||||||
|
dnsProvider,
|
||||||
|
csrInfo: options.cert.csrInfo,
|
||||||
|
isTest: options.args.test
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.writeCert(cert)
|
||||||
|
const certRet = await this.readCurrentCert()
|
||||||
|
certRet.isNew = true
|
||||||
|
return certRet
|
||||||
|
}
|
||||||
|
|
||||||
|
createDnsProvider (options) {
|
||||||
|
const accessProviders = options.accessProviders
|
||||||
|
const providerOptions = accessProviders[options.cert.dnsProvider]
|
||||||
|
return this.createProviderByType(providerOptions.providerType, providerOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeCert (cert) {
|
||||||
|
const newPath = await this.certStore.writeCert(cert)
|
||||||
|
return {
|
||||||
|
realPath: this.certStore.store.getActualKey(newPath),
|
||||||
|
currentPath: this.certStore.store.getActualKey(this.certStore.currentRootPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readCurrentCert () {
|
||||||
|
const cert = await this.certStore.readCert()
|
||||||
|
if (cert == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const { detail, expires } = this.getCrtDetail(cert.crt)
|
||||||
|
const domain = this.certStore.getMainDomain(this.options.cert.domains)
|
||||||
|
return {
|
||||||
|
...cert, detail, expires, domain, domains: this.domains, email: this.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getCrtDetail (crt) {
|
||||||
|
const pki = forge.pki
|
||||||
|
const detail = pki.certificateFromPem(crt.toString())
|
||||||
|
const expires = detail.validity.notAfter
|
||||||
|
return { detail, expires }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否过期,默认提前20天
|
||||||
|
* @param expires
|
||||||
|
* @param maxDays
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isWillExpire (expires, maxDays = 20) {
|
||||||
|
if (expires == null) {
|
||||||
|
throw new Error('过期时间不能为空')
|
||||||
|
}
|
||||||
|
// 检查有效期
|
||||||
|
const leftDays = dayjs(expires).diff(dayjs(), 'day')
|
||||||
|
return {
|
||||||
|
isWillExpire: leftDays < maxDays,
|
||||||
|
leftDays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createProviderByType (type, options) {
|
||||||
|
try {
|
||||||
|
const Provider = AccessProviderClasses[type]
|
||||||
|
return new Provider(options)
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error('暂不支持此dnsProvider,请先use该provider:' + type, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
111
packages/certd/src/store/cert-store.js
Normal file
111
packages/certd/src/store/cert-store.js
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import crypto from 'crypto'
|
||||||
|
function md5 (content) {
|
||||||
|
return crypto.createHash('md5').update(content).digest('hex')
|
||||||
|
}
|
||||||
|
export class CertStore {
|
||||||
|
constructor ({ store, email, domains }) {
|
||||||
|
this.store = store
|
||||||
|
this.email = email
|
||||||
|
this.domains = domains
|
||||||
|
this.domain = this.getMainDomain(this.domains)
|
||||||
|
this.safetyDomain = this.getSafetyDomain(this.domain)
|
||||||
|
this.domainDir = this.safetyDomain + '-' + md5(this.getDomainStr(this.domains))
|
||||||
|
this.certsRootPath = this.store.buildKey(this.email, 'certs')
|
||||||
|
|
||||||
|
this.currentRootPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current')
|
||||||
|
}
|
||||||
|
|
||||||
|
getMainDomain (domains) {
|
||||||
|
if (domains == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof domains === 'string') {
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
if (domains.length > 0) {
|
||||||
|
return domains[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDomainStr (domains) {
|
||||||
|
if (domains == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (typeof domains === 'string') {
|
||||||
|
return domains
|
||||||
|
}
|
||||||
|
return domains.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
buildNewCertRootPath (dir) {
|
||||||
|
if (dir == null) {
|
||||||
|
dir = dayjs().format('YYYY.MM.DD.HHmmss')
|
||||||
|
}
|
||||||
|
return this.store.buildKey(this.certsRootPath, this.domainDir, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatCert (pem) {
|
||||||
|
pem = pem.replace(/\r/g, '')
|
||||||
|
pem = pem.replace(/\n\n/g, '\n')
|
||||||
|
pem = pem.replace(/\n$/g, '')
|
||||||
|
return pem
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeCert (cert) {
|
||||||
|
const newDir = this.buildNewCertRootPath()
|
||||||
|
|
||||||
|
const crtKey = this.buildKey(newDir, this.safetyDomain + '.crt')
|
||||||
|
const priKey = this.buildKey(newDir, this.safetyDomain + '.key')
|
||||||
|
const csrKey = this.buildKey(newDir, this.safetyDomain + '.csr')
|
||||||
|
await this.store.set(crtKey, this.formatCert(cert.crt.toString()))
|
||||||
|
await this.store.set(priKey, this.formatCert(cert.key.toString()))
|
||||||
|
await this.store.set(csrKey, cert.csr.toString())
|
||||||
|
|
||||||
|
await this.store.link(newDir, this.currentRootPath)
|
||||||
|
|
||||||
|
return newDir
|
||||||
|
}
|
||||||
|
|
||||||
|
async readCert (dir) {
|
||||||
|
if (dir == null) {
|
||||||
|
dir = this.currentRootPath
|
||||||
|
}
|
||||||
|
const crtKey = this.buildKey(dir, this.safetyDomain + '.crt')
|
||||||
|
const priKey = this.buildKey(dir, this.safetyDomain + '.key')
|
||||||
|
const csrKey = this.buildKey(dir, this.safetyDomain + '.csr')
|
||||||
|
const crt = await this.store.get(crtKey)
|
||||||
|
if (crt == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const key = await this.store.get(priKey)
|
||||||
|
const csr = await this.store.get(csrKey)
|
||||||
|
|
||||||
|
return {
|
||||||
|
crt: this.formatCert(crt),
|
||||||
|
key: this.formatCert(key),
|
||||||
|
csr,
|
||||||
|
crtPath: this.store.getActualKey(crtKey),
|
||||||
|
keyPath: this.store.getActualKey(priKey),
|
||||||
|
certDir: this.store.getActualKey(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildKey (...keyItem) {
|
||||||
|
return this.store.buildKey(...keyItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
getSafetyDomain (domain) {
|
||||||
|
return domain.replace(/\*/g, '_')
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentFile (file) {
|
||||||
|
const key = this.buildKey(this.currentRootPath, file)
|
||||||
|
return this.store.get(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentFile (file, value) {
|
||||||
|
const key = this.buildKey(this.currentRootPath, file)
|
||||||
|
return this.store.set(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
66
packages/certd/src/store/file-store.js
Normal file
66
packages/certd/src/store/file-store.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { Store, util } from '@certd/api'
|
||||||
|
import path from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
const logger = util.logger
|
||||||
|
export class FileStore extends Store {
|
||||||
|
constructor (opts) {
|
||||||
|
super()
|
||||||
|
if (opts.rootDir != null) {
|
||||||
|
this.rootDir = opts.rootDir
|
||||||
|
} else {
|
||||||
|
this.rootDir = util.path.getUserBasePath()
|
||||||
|
}
|
||||||
|
if (opts.test) {
|
||||||
|
this.rootDir = path.join(this.rootDir, '/test/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getActualKey (key) {
|
||||||
|
// return 前缀+key
|
||||||
|
return this.getPathByKey(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildKey (...keyItem) {
|
||||||
|
return path.join(...keyItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
getPathByKey (key) {
|
||||||
|
return path.join(this.rootDir, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
set (key, value) {
|
||||||
|
const filePath = this.getPathByKey(key)
|
||||||
|
const dir = path.dirname(filePath)
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, value)
|
||||||
|
return filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
get (key) {
|
||||||
|
const filePath = this.getPathByKey(key)
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return fs.readFileSync(filePath).toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
link (targetPath, linkPath) {
|
||||||
|
targetPath = this.getPathByKey(targetPath)
|
||||||
|
linkPath = this.getPathByKey(linkPath)
|
||||||
|
if (fs.existsSync(linkPath)) {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(linkPath)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('unlink error:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs.symlinkSync(targetPath, linkPath, 'dir')
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink (linkPath) {
|
||||||
|
linkPath = this.getPathByKey(linkPath)
|
||||||
|
fs.unlinkSync(linkPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
18
packages/certd/test/cert-apply/aliyun.test.js
Normal file
18
packages/certd/test/cert-apply/aliyun.test.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
import { Certd } from '../../src/index.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('AliyunDnsProvider', function () {
|
||||||
|
it('#申请证书-aliyun', async function () {
|
||||||
|
this.timeout(300000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.args = { forceCert: true, test: false }
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.certApply()
|
||||||
|
expect(cert).ok
|
||||||
|
expect(cert.crt).ok
|
||||||
|
expect(cert.key).ok
|
||||||
|
expect(cert.detail).ok
|
||||||
|
expect(cert.expires).ok
|
||||||
|
})
|
||||||
|
})
|
||||||
20
packages/certd/test/cert-apply/dnspod.test.js
Normal file
20
packages/certd/test/cert-apply/dnspod.test.js
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { Certd } from '../../src/index.js'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('DnspodDnsProvider', function () {
|
||||||
|
it('#申请证书', async function () {
|
||||||
|
this.timeout(300000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.cert.domains = ['*.certd.xyz', '*.test.certd.xyz', '*.base.certd.xyz', 'certd.xyz']
|
||||||
|
options.cert.dnsProvider = 'dnspod'
|
||||||
|
options.args = { forceCert: true }
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.certApply()
|
||||||
|
expect(cert).ok
|
||||||
|
expect(cert.crt).ok
|
||||||
|
expect(cert.key).ok
|
||||||
|
expect(cert.detail).ok
|
||||||
|
expect(cert.expires).ok
|
||||||
|
})
|
||||||
|
})
|
||||||
88
packages/certd/test/index.test.js
Normal file
88
packages/certd/test/index.test.js
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import chai from 'chai'
|
||||||
|
import { Certd } from '../src/index.js'
|
||||||
|
import { createOptions } from '../../../test/options.js'
|
||||||
|
const { expect } = chai
|
||||||
|
const fakeCrt = `-----BEGIN CERTIFICATE-----
|
||||||
|
MIIFSTCCBDGgAwIBAgITAPoZZk/LhVIyXoic2NnJyxubezANBgkqhkiG9w0BAQsF
|
||||||
|
ADAiMSAwHgYDVQQDDBdGYWtlIExFIEludGVybWVkaWF0ZSBYMTAeFw0yMDEyMTQx
|
||||||
|
NjA1NTFaFw0yMTAzMTQxNjA1NTFaMBsxGTAXBgNVBAMMECouZG9jbWlycm9yLmNs
|
||||||
|
dWIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC75tGrYjly+RpcZehQ
|
||||||
|
my1EpaXElT4L60pINKV2YDKnBrcSSo1c6rO7nFh12eC/ju4WwYUep0RVmBDF8xD0
|
||||||
|
I1Sd1uuDTQWP0UT1X9yqdXtjvxpUqoCHAzG633f3sJRFul7mDLuC9tRCuae9o7qP
|
||||||
|
EZ827XOmjBR35dso9I2GEE4828J3YE3tSKtobZlM+30jozLEcsO0PTyM5mq5PPjP
|
||||||
|
VI3fGLcEaBmLZf5ixz4XkcY9IAhyAMYf03cT2wRoYPBaDdXblgCYL6sFtIMbzl3M
|
||||||
|
Di94PB8NyoNSsC2nmBdWi54wFOgBvY/4ljsX/q7X3EqlSvcA0/M6/c/J9kJ3eupv
|
||||||
|
jV8nAgMBAAGjggJ9MIICeTAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYB
|
||||||
|
BQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFAkdTjSCV3KD
|
||||||
|
x28sf98MrwVfyFYgMB8GA1UdIwQYMBaAFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHcG
|
||||||
|
CCsGAQUFBwEBBGswaTAyBggrBgEFBQcwAYYmaHR0cDovL29jc3Auc3RnLWludC14
|
||||||
|
MS5sZXRzZW5jcnlwdC5vcmcwMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0Zy1p
|
||||||
|
bnQteDEubGV0c2VuY3J5cHQub3JnLzArBgNVHREEJDAighAqLmRvY21pcnJvci5j
|
||||||
|
bHVigg5kb2NtaXJyb3IuY2x1YjBMBgNVHSAERTBDMAgGBmeBDAECATA3BgsrBgEE
|
||||||
|
AYLfEwEBATAoMCYGCCsGAQUFBwIBFhpodHRwOi8vY3BzLmxldHNlbmNyeXB0Lm9y
|
||||||
|
ZzCCAQQGCisGAQQB1nkCBAIEgfUEgfIA8AB1ABboacHRlerXw/iXGuPwdgH3jOG2
|
||||||
|
nTGoUhi2g38xqBUIAAABdmI3LM4AAAQDAEYwRAIgaiNqXSEq+sxp8eqlJXp/KFdO
|
||||||
|
so5mT50MoRsLF8Inu0ACIDP46+ekng7I0BlmyIPmbqFcZgnZFVWLLCdLYijhVyOL
|
||||||
|
AHcA3Zk0/KXnJIDJVmh9gTSZCEmySfe1adjHvKs/XMHzbmQAAAF2YjcuxwAABAMA
|
||||||
|
SDBGAiEAxpeB8/w4YkHZ62nH20h128VtuTSmYDCnF7EK2fQyeZYCIQDbJlF2wehZ
|
||||||
|
sF1BeE7qnYYqCTP0dYIrQ9HWtBa/MbGOKTANBgkqhkiG9w0BAQsFAAOCAQEAL2di
|
||||||
|
HKh6XcZtGk0BFxJa51sCZ3MLu9+Zy90kCRD4ooP5x932WxVM25+LBRd+xSzx+TRL
|
||||||
|
UVrlKp9GdMYX1JXL4Vf2NwzuFO3snPDe/qizD/3+D6yo8eKJ/LD82t5kLWAD2rto
|
||||||
|
YfVSTKwfNIBBJwHUnjviBPJmheHHCKmz8Ct6/6QxFAeta9TAMn0sFeVCQnmAq7HL
|
||||||
|
jrunq0tNHR/EKG0ITPLf+6P7MxbmpYNnq918766l0tKsW8oo8ZSGEwKU2LMaSiAa
|
||||||
|
hasyl/2gMnYXjtKOjDcnR8oLpbrOg0qpVbynmJin1HP835oHPPAZ1gLsqYTTizNz
|
||||||
|
AHxTaXliTVvS83dogw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEqzCCApOgAwIBAgIRAIvhKg5ZRO08VGQx8JdhT+UwDQYJKoZIhvcNAQELBQAw
|
||||||
|
GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDUyMzIyMDc1OVoXDTM2
|
||||||
|
MDUyMzIyMDc1OVowIjEgMB4GA1UEAwwXRmFrZSBMRSBJbnRlcm1lZGlhdGUgWDEw
|
||||||
|
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDtWKySDn7rWZc5ggjz3ZB0
|
||||||
|
8jO4xti3uzINfD5sQ7Lj7hzetUT+wQob+iXSZkhnvx+IvdbXF5/yt8aWPpUKnPym
|
||||||
|
oLxsYiI5gQBLxNDzIec0OIaflWqAr29m7J8+NNtApEN8nZFnf3bhehZW7AxmS1m0
|
||||||
|
ZnSsdHw0Fw+bgixPg2MQ9k9oefFeqa+7Kqdlz5bbrUYV2volxhDFtnI4Mh8BiWCN
|
||||||
|
xDH1Hizq+GKCcHsinDZWurCqder/afJBnQs+SBSL6MVApHt+d35zjBD92fO2Je56
|
||||||
|
dhMfzCgOKXeJ340WhW3TjD1zqLZXeaCyUNRnfOmWZV8nEhtHOFbUCU7r/KkjMZO9
|
||||||
|
AgMBAAGjgeMwgeAwDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAw
|
||||||
|
HQYDVR0OBBYEFMDMA0a5WCDMXHJw8+EuyyCm9Wg6MHoGCCsGAQUFBwEBBG4wbDA0
|
||||||
|
BggrBgEFBQcwAYYoaHR0cDovL29jc3Auc3RnLXJvb3QteDEubGV0c2VuY3J5cHQu
|
||||||
|
b3JnLzA0BggrBgEFBQcwAoYoaHR0cDovL2NlcnQuc3RnLXJvb3QteDEubGV0c2Vu
|
||||||
|
Y3J5cHQub3JnLzAfBgNVHSMEGDAWgBTBJnSkikSg5vogKNhcI5pFiBh54DANBgkq
|
||||||
|
hkiG9w0BAQsFAAOCAgEABYSu4Il+fI0MYU42OTmEj+1HqQ5DvyAeyCA6sGuZdwjF
|
||||||
|
UGeVOv3NnLyfofuUOjEbY5irFCDtnv+0ckukUZN9lz4Q2YjWGUpW4TTu3ieTsaC9
|
||||||
|
AFvCSgNHJyWSVtWvB5XDxsqawl1KzHzzwr132bF2rtGtazSqVqK9E07sGHMCf+zp
|
||||||
|
DQVDVVGtqZPHwX3KqUtefE621b8RI6VCl4oD30Olf8pjuzG4JKBFRFclzLRjo/h7
|
||||||
|
IkkfjZ8wDa7faOjVXx6n+eUQ29cIMCzr8/rNWHS9pYGGQKJiY2xmVC9h12H99Xyf
|
||||||
|
zWE9vb5zKP3MVG6neX1hSdo7PEAb9fqRhHkqVsqUvJlIRmvXvVKTwNCP3eCjRCCI
|
||||||
|
PTAvjV+4ni786iXwwFYNz8l3PmPLCyQXWGohnJ8iBm+5nk7O2ynaPVW0U2W+pt2w
|
||||||
|
SVuvdDM5zGv2f9ltNWUiYZHJ1mmO97jSY/6YfdOUH66iRtQtDkHBRdkNBsMbD+Em
|
||||||
|
2TgBldtHNSJBfB3pm9FblgOcJ0FSWcUDWJ7vO0+NTXlgrRofRT6pVywzxVo6dND0
|
||||||
|
WzYlTWeUVsO40xJqhgUQRER9YLOLxJ0O6C8i0xFxAMKOtSdodMB3RIwt7RFQ0uyt
|
||||||
|
n5Z5MqkYhlMI3J1tPRTp1nEt9fyGspBOO05gi148Qasp+3N+svqKomoQglNoAxU=
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
describe('Certd', function () {
|
||||||
|
it('#buildCertDir', function () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.club']
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const currentRootPath = certd.certStore.currentRootPath
|
||||||
|
console.log('rootDir', currentRootPath)
|
||||||
|
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club-\w+\\current/)
|
||||||
|
})
|
||||||
|
it('#writeAndReadCert', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.domain.cn']
|
||||||
|
const certd = new Certd(options)
|
||||||
|
await certd.writeCert({ csr: 'csr', crt: fakeCrt, key: 'bbb' })
|
||||||
|
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
expect(cert).to.be.ok
|
||||||
|
expect(cert.crt).ok
|
||||||
|
expect(cert.key).to.be.ok
|
||||||
|
expect(cert.detail).to.be.ok
|
||||||
|
expect(cert.expires).to.be.ok
|
||||||
|
console.log('expires:', cert.expires)
|
||||||
|
})
|
||||||
|
})
|
||||||
14
packages/executor/.eslintrc
Normal file
14
packages/executor/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "standard",
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.js", "*.spec.js"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3920
packages/executor/package-lock.json
generated
Normal file
3920
packages/executor/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
36
packages/executor/package.json
Normal file
36
packages/executor/package.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "@certd/executor",
|
||||||
|
"version": "0.1.11",
|
||||||
|
"description": "",
|
||||||
|
"main": "src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "webpack --config webpack.config.cjs ",
|
||||||
|
"rollup": "rollup --config rollup.config.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@certd/api": "^0.1.11",
|
||||||
|
"@certd/certd": "^0.1.11",
|
||||||
|
"@certd/plugins": "^0.1.11",
|
||||||
|
"dayjs": "^1.9.7",
|
||||||
|
"lodash-es": "^4.17.20"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@rollup/plugin-commonjs": "^17.0.0",
|
||||||
|
"@rollup/plugin-json": "^4.1.0",
|
||||||
|
"@rollup/plugin-node-resolve": "^11.0.1",
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^7.15.0",
|
||||||
|
"eslint-config-standard": "^16.0.2",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"mocha": "^8.2.1",
|
||||||
|
"rollup": "^2.35.1",
|
||||||
|
"rollup-plugin-terser": "^7.0.2"
|
||||||
|
},
|
||||||
|
"author": "Greper",
|
||||||
|
"license": "MIT",
|
||||||
|
"sideEffects": false
|
||||||
|
}
|
||||||
21
packages/executor/rollup.config.js
Normal file
21
packages/executor/rollup.config.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import json from '@rollup/plugin-json'
|
||||||
|
import { terser } from 'rollup-plugin-terser'
|
||||||
|
import commonjs from '@rollup/plugin-commonjs'
|
||||||
|
import { nodeResolve } from '@rollup/plugin-node-resolve'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: 'src/index.js',
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'bundle.js',
|
||||||
|
format: 'es'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'bundle.min.js',
|
||||||
|
format: 'iife',
|
||||||
|
name: 'version',
|
||||||
|
plugins: [terser()]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [json(), commonjs(), nodeResolve()]
|
||||||
|
}
|
||||||
198
packages/executor/src/index.js
Normal file
198
packages/executor/src/index.js
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import DefaultPlugins from '@certd/plugins'
|
||||||
|
import { util } from '@certd/api'
|
||||||
|
import _ from 'lodash-es'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { Trace } from './trace.js'
|
||||||
|
const logger = util.logger
|
||||||
|
|
||||||
|
function createDefaultOptions () {
|
||||||
|
return {
|
||||||
|
args: {
|
||||||
|
forceCert: false,
|
||||||
|
forceDeploy: true,
|
||||||
|
forceRedeploy: false,
|
||||||
|
doNotThrowError: false // 部署流程执行有错误时,不抛异常,此时整个任务执行完毕后,可以返回结果,你可以在返回结果中处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Executor {
|
||||||
|
constructor () {
|
||||||
|
this.usePlugins(DefaultPlugins)
|
||||||
|
this.trace = new Trace()
|
||||||
|
}
|
||||||
|
|
||||||
|
useProviders (providers) {
|
||||||
|
if (providers) {
|
||||||
|
_.forEach(item => {
|
||||||
|
Certd.use(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useProvider (provider) {
|
||||||
|
Certd.use(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
usePlugin (plugin) {
|
||||||
|
if (plugin == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.plugins == null) {
|
||||||
|
this.plugins = {}
|
||||||
|
}
|
||||||
|
this.plugins[plugin.name] = plugin
|
||||||
|
if (plugin.define) {
|
||||||
|
const define = plugin.define()
|
||||||
|
this.plugins[define.name] = plugin
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usePlugins (plugins) {
|
||||||
|
if (plugins) {
|
||||||
|
_.forEach(plugins, item => {
|
||||||
|
this.usePlugin(item)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async run (options) {
|
||||||
|
logger.info('------------------- Cert-D ---------------------')
|
||||||
|
try {
|
||||||
|
options = _.merge(createDefaultOptions(), options)
|
||||||
|
return await this.doRun(options)
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('任务执行出错:', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRun (options) {
|
||||||
|
// 申请证书
|
||||||
|
logger.info('任务开始')
|
||||||
|
const certd = new Certd(options, this.providers)
|
||||||
|
const cert = await this.runCertd(certd)
|
||||||
|
if (cert == null) {
|
||||||
|
throw new Error('申请证书失败')
|
||||||
|
}
|
||||||
|
logger.info('证书保存路径:', cert.certDir)
|
||||||
|
|
||||||
|
logger.info('----------------------')
|
||||||
|
if (!cert.isNew) {
|
||||||
|
// 如果没有更新
|
||||||
|
if (!options.args.forceDeploy && !options.args.forceRedeploy) {
|
||||||
|
// 且不需要强制运行deploy
|
||||||
|
logger.info('证书无更新,无需重新部署')
|
||||||
|
logger.info('任务完成')
|
||||||
|
return { cert }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 读取上次执行进度
|
||||||
|
let context = {}
|
||||||
|
const contextJson = await certd.certStore.getCurrentFile('context.json')
|
||||||
|
if (contextJson) {
|
||||||
|
context = JSON.parse(contextJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.certIsNew = !!cert.isNew
|
||||||
|
|
||||||
|
const trace = new Trace(context)
|
||||||
|
const resultTrace = trace.getInstance({ type: 'result' })
|
||||||
|
// 运行部署任务
|
||||||
|
try {
|
||||||
|
await this.runDeploys({ options, cert, context, trace })
|
||||||
|
} finally {
|
||||||
|
await certd.certStore.setCurrentFile('context.json', JSON.stringify(context))
|
||||||
|
}
|
||||||
|
logger.info('任务完成')
|
||||||
|
trace.print()
|
||||||
|
const result = resultTrace.get({ })
|
||||||
|
const returnData = {
|
||||||
|
cert,
|
||||||
|
context,
|
||||||
|
result
|
||||||
|
}
|
||||||
|
if (result.status === 'error' && options.args.doNotThrowError === false) {
|
||||||
|
throw new Error(result.remark)
|
||||||
|
}
|
||||||
|
return returnData
|
||||||
|
}
|
||||||
|
|
||||||
|
async runCertd (certd) {
|
||||||
|
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 开始`)
|
||||||
|
const cert = await certd.certApply()
|
||||||
|
logger.info(`证书任务 ${JSON.stringify(certd.options.cert.domains)} 完成`)
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
|
||||||
|
async runDeploys ({ options, cert, context, trace }) {
|
||||||
|
if (cert == null) {
|
||||||
|
const certd = new Certd(options)
|
||||||
|
cert = await certd.readCurrentCert()
|
||||||
|
}
|
||||||
|
logger.info('部署任务开始')
|
||||||
|
for (const deploy of options.deploy) {
|
||||||
|
const deployName = deploy.deployName
|
||||||
|
logger.info(`------------【${deployName}】-----------`)
|
||||||
|
|
||||||
|
const deployTrace = trace.getInstance({ type: 'deploy', deployName })
|
||||||
|
if (deploy.disabled === true) {
|
||||||
|
logger.info('此流程已被禁用,跳过')
|
||||||
|
logger.info('')
|
||||||
|
deployTrace.set({ value: { current: 'skip', status: 'disabled', remark: '流程禁用' } })
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const task of deploy.tasks) {
|
||||||
|
if (context[deployName] == null) {
|
||||||
|
context[deployName] = {}
|
||||||
|
}
|
||||||
|
const taskContext = context[deployName]
|
||||||
|
// 开始执行任务列表
|
||||||
|
await this.runTask({ options, cert, task, context: taskContext, deploy, trace })
|
||||||
|
}
|
||||||
|
|
||||||
|
deployTrace.set({ value: { status: 'success', remark: '执行成功' } })
|
||||||
|
} catch (e) {
|
||||||
|
deployTrace.set({ value: { status: 'error', remark: '执行失败:' + e.message } })
|
||||||
|
trace.set({ type: 'result', value: { status: 'error', remark: deployName + '执行失败:' + e.message } })
|
||||||
|
logger.error('流程执行失败', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async runTask ({ options, task, cert, context, deploy, trace }) {
|
||||||
|
const taskType = task.type
|
||||||
|
const Plugin = this.plugins[taskType]
|
||||||
|
const deployName = deploy.deployName
|
||||||
|
const taskName = task.taskName
|
||||||
|
if (Plugin == null) {
|
||||||
|
throw new Error(`插件:${taskType}还未安装`)
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance = Plugin
|
||||||
|
if (Plugin instanceof Function) {
|
||||||
|
instance = new Plugin({ accessProviders: options.accessProviders })
|
||||||
|
}
|
||||||
|
const taskTrace = trace.getInstance({ type: 'deploy', deployName, taskName })
|
||||||
|
const traceStatus = taskTrace.get({})
|
||||||
|
if (traceStatus && traceStatus.status === 'success' && !options.args.forceRedeploy) {
|
||||||
|
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
|
||||||
|
taskTrace.set({ value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.info(`----【${taskName}】开始执行`)
|
||||||
|
try {
|
||||||
|
// 执行任务
|
||||||
|
await instance.execute({ cert, props: task.props, context })
|
||||||
|
taskTrace.set({ value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
|
||||||
|
} catch (e) {
|
||||||
|
taskTrace.set({ value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
logger.info(`----任务【${taskName}】执行完成`)
|
||||||
|
logger.info('')
|
||||||
|
}
|
||||||
|
}
|
||||||
94
packages/executor/src/trace.js
Normal file
94
packages/executor/src/trace.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { util } from '@certd/api'
|
||||||
|
import _ from 'lodash-es'
|
||||||
|
const logger = util.logger
|
||||||
|
export class Trace {
|
||||||
|
constructor (context) {
|
||||||
|
this.context = context
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstance ({ type, deployName, taskName }) {
|
||||||
|
return {
|
||||||
|
get: ({ prop }) => {
|
||||||
|
return this.get({ type, deployName, taskName, prop })
|
||||||
|
},
|
||||||
|
set: ({ prop, value }) => {
|
||||||
|
this.set({ type, deployName, taskName, prop, value })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set ({ type, deployName, taskName, prop, value }) {
|
||||||
|
const key = this.buildTraceKey({ type, deployName, taskName, prop })
|
||||||
|
const oldValue = _.get(this.context, key) || {}
|
||||||
|
_.merge(oldValue, value)
|
||||||
|
_.set(this.context, key, oldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
get ({ type, deployName, taskName, prop }) {
|
||||||
|
return _.get(this.context, this.buildTraceKey({ type, deployName, taskName, prop }))
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTraceKey ({ type = 'default', deployName, taskName, prop }) {
|
||||||
|
let key = '__trace__.' + type
|
||||||
|
if (deployName) {
|
||||||
|
key += '.'
|
||||||
|
key += deployName.replace(/\./g, '_')
|
||||||
|
}
|
||||||
|
if (taskName) {
|
||||||
|
key += '.tasks.'
|
||||||
|
key += taskName.replace(/\./g, '_')
|
||||||
|
}
|
||||||
|
if (prop) {
|
||||||
|
key += '.' + prop
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
getStringLength (str) {
|
||||||
|
const enLength = str.replace(/[\u0391-\uFFE5]/g, '').length // 先把中文替换成两个字节的英文,再计算长度
|
||||||
|
return Math.floor((str.length - enLength) * 1.5) + enLength
|
||||||
|
}
|
||||||
|
|
||||||
|
print () {
|
||||||
|
const context = this.context
|
||||||
|
logger.info('---------------------------任务结果总览--------------------------')
|
||||||
|
if (context.certIsNew) {
|
||||||
|
this.printTraceLine({ current: 'success', remark: '证书更新成功' }, '更新证书')
|
||||||
|
} else {
|
||||||
|
this.printTraceLine({ current: 'skip', remark: '还未到过期时间,跳过' }, '更新证书')
|
||||||
|
}
|
||||||
|
const trace = this.get({ type: 'deploy' })
|
||||||
|
// logger.info('trace', trace)
|
||||||
|
for (const deployName in trace) {
|
||||||
|
if (trace[deployName] == null) {
|
||||||
|
trace[deployName] = {}
|
||||||
|
}
|
||||||
|
const traceStatus = this.printTraceLine(trace[deployName], deployName)
|
||||||
|
|
||||||
|
const tasks = traceStatus.tasks
|
||||||
|
if (tasks) {
|
||||||
|
for (const taskName in tasks) {
|
||||||
|
if (tasks[taskName] == null) {
|
||||||
|
tasks[taskName] = {}
|
||||||
|
}
|
||||||
|
this.printTraceLine(tasks[taskName], taskName, ' └')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const result = this.get({ type: 'result' })
|
||||||
|
this.printTraceLine(result, 'result', '')
|
||||||
|
const mainContext = {}
|
||||||
|
_.merge(mainContext, context)
|
||||||
|
delete mainContext.__trace__
|
||||||
|
logger.info('【context】', JSON.stringify(mainContext))
|
||||||
|
}
|
||||||
|
|
||||||
|
printTraceLine (traceStatus, name, prefix = '') {
|
||||||
|
const length = this.getStringLength(name)
|
||||||
|
const endPad = _.repeat('-', 45 - prefix.length - length) + '\t'
|
||||||
|
const status = traceStatus.current || traceStatus.status || ''
|
||||||
|
const remark = traceStatus.remark || ''
|
||||||
|
logger.info(`${prefix}【${name}】${endPad}[${status}] \t${remark}`)
|
||||||
|
return traceStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
34
packages/executor/test/index.test.js
Normal file
34
packages/executor/test/index.test.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { Executor } from '../src/index.js'
|
||||||
|
import { createOptions } from '../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
|
||||||
|
describe('AutoDeploy', function () {
|
||||||
|
it('#run', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
const options = createOptions()
|
||||||
|
const executor = new Executor()
|
||||||
|
const ret = await executor.run(options)
|
||||||
|
expect(ret).ok
|
||||||
|
expect(ret.cert).ok
|
||||||
|
})
|
||||||
|
it('#forceCert', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
const executor = new Executor()
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.forceCert = true
|
||||||
|
options.args.forceDeploy = true
|
||||||
|
|
||||||
|
const ret = await executor.run(options)
|
||||||
|
expect(ret).ok
|
||||||
|
expect(ret.cert).ok
|
||||||
|
})
|
||||||
|
it('#forceDeploy', async function () {
|
||||||
|
this.timeout(120000)
|
||||||
|
const executor = new Executor()
|
||||||
|
const options = createOptions()
|
||||||
|
const ret = await executor.run(options, { forceCert: false, forceDeploy: true, forceRedeploy: true })
|
||||||
|
expect(ret).ok
|
||||||
|
expect(ret.cert).ok
|
||||||
|
})
|
||||||
|
})
|
||||||
23
packages/executor/webpack.config.cjs
Normal file
23
packages/executor/webpack.config.cjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
const path = require('path')
|
||||||
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
|
||||||
|
console.log(CleanWebpackPlugin)
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
devtool: 'source-map',
|
||||||
|
target: 'node',
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
filename: 'executor.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
library: 'certdExecutor',
|
||||||
|
libraryTarget: 'umd'
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin()
|
||||||
|
],
|
||||||
|
mode: 'production'
|
||||||
|
// mode: 'development',
|
||||||
|
// optimization: {
|
||||||
|
// usedExports: true
|
||||||
|
// }
|
||||||
|
}
|
||||||
3136
packages/executor/yarn.lock
Normal file
3136
packages/executor/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
14
packages/plugins/.eslintrc
Normal file
14
packages/plugins/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "standard",
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.js", "*.spec.js"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3690
packages/plugins/package-lock.json
generated
Normal file
3690
packages/plugins/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
packages/plugins/package.json
Normal file
27
packages/plugins/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@certd/plugins",
|
||||||
|
"version": "0.1.11",
|
||||||
|
"description": "",
|
||||||
|
"main": "./src/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
|
"@certd/api": "^0.1.11",
|
||||||
|
"dayjs": "^1.9.7",
|
||||||
|
"kubernetes-client": "^9.0.0",
|
||||||
|
"lodash-es": "^4.17.20",
|
||||||
|
"ssh2": "^0.8.9",
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.0.44"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^7.15.0",
|
||||||
|
"eslint-config-standard": "^16.0.2",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"mocha": "^8.2.1"
|
||||||
|
},
|
||||||
|
"author": "Greper",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
9
packages/plugins/src/aliyun/abstract-aliyun.js
Normal file
9
packages/plugins/src/aliyun/abstract-aliyun.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { AbstractPlugin } from '@certd/api'
|
||||||
|
|
||||||
|
export class AbstractAliyunPlugin extends AbstractPlugin {
|
||||||
|
checkRet (ret) {
|
||||||
|
if (ret.code != null) {
|
||||||
|
throw new Error('执行失败:', ret.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
packages/plugins/src/aliyun/deploy-to-cdn/index.js
Normal file
94
packages/plugins/src/aliyun/deploy-to-cdn/index.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { AbstractAliyunPlugin } from '../../aliyun/abstract-aliyun.js'
|
||||||
|
import Core from '@alicloud/pop-core'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
export class DeployCertToAliyunCDN extends AbstractAliyunPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'deployCertToAliyunCDN',
|
||||||
|
label: '部署到阿里云CDN',
|
||||||
|
input: {
|
||||||
|
domainName: {
|
||||||
|
label: 'cdn加速域名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
certName: {
|
||||||
|
label: '证书名称'
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
value: 'upload',
|
||||||
|
label: '证书来源',
|
||||||
|
options: [
|
||||||
|
{ value: 'upload', label: '直接上传' },
|
||||||
|
{ value: 'cas', label: '从证书库', desc: '需要uploadCertToAliyun作为前置任务' }
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// serverCertificateStatus: {
|
||||||
|
// label: '启用https',
|
||||||
|
// options: [
|
||||||
|
// { value: 'on', label: '开启HTTPS,并更新证书' },
|
||||||
|
// { value: 'auto', label: '若HTTPS开启则更新,未开启不更新' }
|
||||||
|
// ],
|
||||||
|
// required:true
|
||||||
|
// },
|
||||||
|
accessProvider: {
|
||||||
|
label: 'Access提供者',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
|
||||||
|
options: 'accessProviders[type=aliyun]',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||||
|
const client = this.getClient(accessProvider)
|
||||||
|
const params = this.buildParams(props, context, cert)
|
||||||
|
await this.doRequest(client, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient (aliyunProvider) {
|
||||||
|
return new Core({
|
||||||
|
accessKeyId: aliyunProvider.accessKeyId,
|
||||||
|
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||||
|
endpoint: 'https://cdn.aliyuncs.com',
|
||||||
|
apiVersion: '2018-05-10'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParams (args, context, cert) {
|
||||||
|
const { certName, from, domainName } = args
|
||||||
|
const CertName = certName + '-' + dayjs().format('YYYYMMDDHHmmss')
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
RegionId: 'cn-hangzhou',
|
||||||
|
DomainName: domainName,
|
||||||
|
ServerCertificateStatus: 'on',
|
||||||
|
CertName: CertName,
|
||||||
|
CertType: from,
|
||||||
|
ServerCertificate: cert.crt,
|
||||||
|
PrivateKey: cert.key
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRequest (client, params) {
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
const ret = await client.request('SetDomainServerCertificate', params, requestOption)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('设置cdn证书成功:', ret.RequestId)
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/plugins/src/aliyun/upload-to-aliyun/index.js
Normal file
97
packages/plugins/src/aliyun/upload-to-aliyun/index.js
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import Core from '@alicloud/pop-core'
|
||||||
|
import { AbstractAliyunPlugin } from '../abstract-aliyun.js'
|
||||||
|
export class UploadCertToAliyun extends AbstractAliyunPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'uploadCertToAliyun',
|
||||||
|
label: '上传证书到阿里云',
|
||||||
|
input: {
|
||||||
|
name: {
|
||||||
|
label: '证书名称'
|
||||||
|
},
|
||||||
|
regionId: {
|
||||||
|
label: '大区',
|
||||||
|
value: 'cn-hangzhou'
|
||||||
|
},
|
||||||
|
accessProvider: {
|
||||||
|
label: 'Access提供者',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
|
||||||
|
options: 'accessProviders[type=aliyun]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
aliyunCertId: {
|
||||||
|
type: String,
|
||||||
|
desc: '上传成功后的阿里云CertId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient (aliyunProvider) {
|
||||||
|
return new Core({
|
||||||
|
accessKeyId: aliyunProvider.accessKeyId,
|
||||||
|
accessKeySecret: aliyunProvider.accessKeySecret,
|
||||||
|
endpoint: 'https://cas.aliyuncs.com',
|
||||||
|
apiVersion: '2018-07-13'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const { name, accessProvider } = props
|
||||||
|
const certName = this.appendTimeSuffix(name || cert.domain)
|
||||||
|
const params = {
|
||||||
|
RegionId: props.regionId || 'cn-hangzhou',
|
||||||
|
Name: certName,
|
||||||
|
Cert: cert.crt,
|
||||||
|
Key: cert.key
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = this.getAccessProvider(accessProvider)
|
||||||
|
const client = this.getClient(provider)
|
||||||
|
const ret = await client.request('CreateUserCertificate', params, requestOption)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('证书上传成功:aliyunCertId=', ret.CertId)
|
||||||
|
context.aliyunCertId = ret.CertId
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 没用,现在阿里云证书不允许删除
|
||||||
|
* @param accessProviders
|
||||||
|
* @param cert
|
||||||
|
* @param props
|
||||||
|
* @param context
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async rollback ({ cert, props, context }) {
|
||||||
|
const { accessProvider } = props
|
||||||
|
const { aliyunCertId } = context
|
||||||
|
this.logger.info('准备删除阿里云证书:', aliyunCertId)
|
||||||
|
const params = {
|
||||||
|
RegionId: props.regionId || 'cn-hangzhou',
|
||||||
|
CertId: aliyunCertId
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
const provider = this.getAccessProvider(accessProvider)
|
||||||
|
const client = this.getClient(provider)
|
||||||
|
const ret = await client.request('DeleteUserCertificate', params, requestOption)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('证书删除成功:', aliyunCertId)
|
||||||
|
delete context.aliyunCertId
|
||||||
|
}
|
||||||
|
}
|
||||||
9
packages/plugins/src/host/abstract-host.js
Normal file
9
packages/plugins/src/host/abstract-host.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { AbstractPlugin } from '@certd/api'
|
||||||
|
|
||||||
|
export class AbstractHostPlugin extends AbstractPlugin {
|
||||||
|
checkRet (ret) {
|
||||||
|
if (ret.code != null) {
|
||||||
|
throw new Error('执行失败:', ret.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
51
packages/plugins/src/host/host-shell-execute/index.js
Normal file
51
packages/plugins/src/host/host-shell-execute/index.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { AbstractHostPlugin } from '../abstract-host.js'
|
||||||
|
import { SshClient } from '../ssh.js'
|
||||||
|
export class HostShellExecute extends AbstractHostPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'hostShellExecute',
|
||||||
|
label: '执行远程主机脚本命令',
|
||||||
|
input: {
|
||||||
|
script: {
|
||||||
|
label: 'shell脚本命令'
|
||||||
|
},
|
||||||
|
accessProvider: {
|
||||||
|
label: '主机登录配置',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含用户名密码的对象',
|
||||||
|
options: 'accessProviders[type=ssh]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const { script, accessProvider } = props
|
||||||
|
const connectConf = this.getAccessProvider(accessProvider)
|
||||||
|
const sshClient = new SshClient()
|
||||||
|
const ret = await sshClient.shell({
|
||||||
|
connectConf,
|
||||||
|
script
|
||||||
|
})
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cert
|
||||||
|
* @param props
|
||||||
|
* @param context
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async rollback ({ cert, props, context }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
110
packages/plugins/src/host/ssh.js
Normal file
110
packages/plugins/src/host/ssh.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import ssh2 from 'ssh2'
|
||||||
|
import logger from '../utils/util.log.js'
|
||||||
|
import path from 'path'
|
||||||
|
export class SshClient {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param connectConf
|
||||||
|
{
|
||||||
|
host: '192.168.100.100',
|
||||||
|
port: 22,
|
||||||
|
username: 'frylock',
|
||||||
|
password: 'nodejsrules'
|
||||||
|
}
|
||||||
|
* @param transports
|
||||||
|
*/
|
||||||
|
uploadFiles ({ connectConf, transports }) {
|
||||||
|
const conn = new ssh2.Client()
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
conn.on('ready', () => {
|
||||||
|
logger.info('连接服务器成功')
|
||||||
|
conn.sftp(async (err, sftp) => {
|
||||||
|
if (err) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const transport of transports) {
|
||||||
|
logger.info('上传文件:', JSON.stringify(transport))
|
||||||
|
await this.exec({ conn, cmd: 'mkdir ' + path.dirname(transport.remotePath) })
|
||||||
|
await this.fastPut({ sftp, ...transport })
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
} finally {
|
||||||
|
conn.end()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).connect(connectConf)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
shell ({ connectConf, script }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.connect({
|
||||||
|
connectConf,
|
||||||
|
onReady: (conn) => {
|
||||||
|
conn.shell((err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const output = []
|
||||||
|
stream.on('close', () => {
|
||||||
|
logger.info('Stream :: close')
|
||||||
|
conn.end()
|
||||||
|
resolve(output)
|
||||||
|
}).on('data', (data) => {
|
||||||
|
logger.info('' + data)
|
||||||
|
output.push('' + data)
|
||||||
|
})
|
||||||
|
stream.end(script + '\nexit\n')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
connect ({ connectConf, onReady }) {
|
||||||
|
const conn = new ssh2.Client()
|
||||||
|
conn.on('ready', () => {
|
||||||
|
console.log('Client :: ready')
|
||||||
|
onReady(conn)
|
||||||
|
}).connect(connectConf)
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
fastPut ({ sftp, localPath, remotePath }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
sftp.fastPut(localPath, remotePath, (err) => {
|
||||||
|
if (err) {
|
||||||
|
reject(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exec ({ conn, cmd }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
conn.exec(cmd, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error('执行命令出错', err)
|
||||||
|
reject(err)
|
||||||
|
// return conn.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.on('close', (code, signal) => {
|
||||||
|
// logger.info('Stream :: close :: code: ' + code + ', signal: ' + signal)
|
||||||
|
// conn.end()
|
||||||
|
resolve()
|
||||||
|
}).on('data', (data) => {
|
||||||
|
logger.info('data', data.toString())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
77
packages/plugins/src/host/upload-to-host/index.js
Normal file
77
packages/plugins/src/host/upload-to-host/index.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { AbstractHostPlugin } from '../abstract-host.js'
|
||||||
|
import { SshClient } from '../ssh.js'
|
||||||
|
export class UploadCertToHost extends AbstractHostPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'uploadCertToHost',
|
||||||
|
label: '上传证书到主机',
|
||||||
|
input: {
|
||||||
|
crtPath: {
|
||||||
|
label: '证书路径'
|
||||||
|
},
|
||||||
|
keyPath: {
|
||||||
|
label: '私钥路径'
|
||||||
|
},
|
||||||
|
accessProvider: {
|
||||||
|
label: '主机登录配置',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含用户名密码的对象',
|
||||||
|
options: 'accessProviders[type=ssh]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
hostCrtPath: {
|
||||||
|
type: String,
|
||||||
|
desc: '上传成功后的证书路径'
|
||||||
|
},
|
||||||
|
hostKeyPath: {
|
||||||
|
type: String,
|
||||||
|
desc: '上传成功后的私钥路径'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const { crtPath, keyPath, accessProvider } = props
|
||||||
|
const connectConf = this.getAccessProvider(accessProvider)
|
||||||
|
const sshClient = new SshClient()
|
||||||
|
await sshClient.uploadFiles({
|
||||||
|
connectConf,
|
||||||
|
transports: [
|
||||||
|
{
|
||||||
|
localPath: cert.crtPath,
|
||||||
|
remotePath: crtPath
|
||||||
|
},
|
||||||
|
{
|
||||||
|
localPath: cert.keyPath,
|
||||||
|
remotePath: keyPath
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
this.logger.info('证书上传成功:crtPath=', crtPath, ',keyPath=', keyPath)
|
||||||
|
|
||||||
|
context.hostCrtPath = crtPath
|
||||||
|
context.hostKeyPath = keyPath
|
||||||
|
return {
|
||||||
|
hostCrtPath: crtPath,
|
||||||
|
hostKeyPath: keyPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param cert
|
||||||
|
* @param props
|
||||||
|
* @param context
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async rollback ({ cert, props, context }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/plugins/src/index.js
Normal file
19
packages/plugins/src/index.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { UploadCertToAliyun } from './aliyun/upload-to-aliyun/index.js'
|
||||||
|
import { DeployCertToAliyunCDN } from './aliyun/deploy-to-cdn/index.js'
|
||||||
|
|
||||||
|
import { UploadCertToTencent } from './tencent/upload-to-tencent/index.js'
|
||||||
|
|
||||||
|
import { DeployCertToTencentCDN } from './tencent/deploy-to-cdn/index.js'
|
||||||
|
|
||||||
|
import { DeployCertToTencentCLB } from './tencent/deploy-to-clb/index.js'
|
||||||
|
|
||||||
|
import { DeployCertToTencentTKEIngress } from './tencent/deploy-to-tke-ingress/index.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
UploadCertToAliyun,
|
||||||
|
DeployCertToAliyunCDN,
|
||||||
|
UploadCertToTencent,
|
||||||
|
DeployCertToTencentTKEIngress,
|
||||||
|
DeployCertToTencentCDN,
|
||||||
|
DeployCertToTencentCLB
|
||||||
|
}
|
||||||
13
packages/plugins/src/tencent/abstract-tencent.js
Normal file
13
packages/plugins/src/tencent/abstract-tencent.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { AbstractPlugin } from '@certd/api'
|
||||||
|
|
||||||
|
export class AbstractTencentPlugin extends AbstractPlugin {
|
||||||
|
checkRet (ret) {
|
||||||
|
if (!ret || ret.Error) {
|
||||||
|
throw new Error('执行失败:' + ret.Error.Code + ',' + ret.Error.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSafetyDomain (domain) {
|
||||||
|
return domain.replace(/\*/g, '_')
|
||||||
|
}
|
||||||
|
}
|
||||||
115
packages/plugins/src/tencent/deploy-to-cdn/index.js
Normal file
115
packages/plugins/src/tencent/deploy-to-cdn/index.js
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { AbstractTencentPlugin } from '../../tencent/abstract-tencent.js'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||||
|
|
||||||
|
export class DeployCertToTencentCDN extends AbstractTencentPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'deployCertToTencentCDN',
|
||||||
|
label: '部署到腾讯云CDN',
|
||||||
|
input: {
|
||||||
|
domainName: {
|
||||||
|
label: 'cdn加速域名',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
certName: {
|
||||||
|
label: '证书名称'
|
||||||
|
},
|
||||||
|
certType: {
|
||||||
|
value: 'upload',
|
||||||
|
label: '证书来源',
|
||||||
|
options: [
|
||||||
|
{ value: 'upload', label: '直接上传' },
|
||||||
|
{ value: 'cloud', label: '从证书库', desc: '需要uploadCertToTencent作为前置任务' }
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// serverCertificateStatus: {
|
||||||
|
// label: '启用https',
|
||||||
|
// options: [
|
||||||
|
// { value: 'on', label: '开启HTTPS,并更新证书' },
|
||||||
|
// { value: 'auto', label: '若HTTPS开启则更新,未开启不更新' }
|
||||||
|
// ],
|
||||||
|
// required:true
|
||||||
|
// },
|
||||||
|
accessProvider: {
|
||||||
|
label: 'Access提供者',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
|
||||||
|
options: 'accessProviders[type=aliyun]',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||||
|
const client = this.getClient(accessProvider)
|
||||||
|
const params = this.buildParams(props, context, cert)
|
||||||
|
await this.doRequest(client, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback ({ cert, props, context }) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient (accessProvider) {
|
||||||
|
const CdnClient = tencentcloud.cdn.v20180606.Client
|
||||||
|
|
||||||
|
const clientConfig = {
|
||||||
|
credential: {
|
||||||
|
secretId: accessProvider.secretId,
|
||||||
|
secretKey: accessProvider.secretKey
|
||||||
|
},
|
||||||
|
region: '',
|
||||||
|
profile: {
|
||||||
|
httpProfile: {
|
||||||
|
endpoint: 'cdn.tencentcloudapi.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CdnClient(clientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
buildParams (props, context, cert) {
|
||||||
|
const { domainName, from } = props
|
||||||
|
const { tencentCertId } = context
|
||||||
|
this.logger.info('部署腾讯云证书ID:', tencentCertId)
|
||||||
|
const params = {
|
||||||
|
Https: {
|
||||||
|
Switch: 'on',
|
||||||
|
CertInfo: {
|
||||||
|
CertId: tencentCertId
|
||||||
|
// Certificate: '1231',
|
||||||
|
// PrivateKey: '1231'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Domain: domainName
|
||||||
|
}
|
||||||
|
if (from === 'upload' || tencentCertId == null) {
|
||||||
|
params.Https.CertInfo = {
|
||||||
|
Certificate: cert.crt,
|
||||||
|
PrivateKey: cert.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRequest (client, params) {
|
||||||
|
const ret = await client.UpdateDomainConfig(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('设置腾讯云CDN证书成功:', ret.RequestId)
|
||||||
|
return ret.RequestId
|
||||||
|
}
|
||||||
|
}
|
||||||
191
packages/plugins/src/tencent/deploy-to-clb/index.js
Normal file
191
packages/plugins/src/tencent/deploy-to-clb/index.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import { AbstractTencentPlugin } from '../../tencent/abstract-tencent.js'
|
||||||
|
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||||
|
export class DeployCertToTencentCLB extends AbstractTencentPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'deployCertToTencentCLB',
|
||||||
|
label: '部署到腾讯云CLB',
|
||||||
|
desc: '暂时只支持单向认证证书,暂时只支持通用负载均衡',
|
||||||
|
input: {
|
||||||
|
region: {
|
||||||
|
label: '大区',
|
||||||
|
value: 'ap-guangzhou'
|
||||||
|
},
|
||||||
|
domain: {
|
||||||
|
label: '域名',
|
||||||
|
type: [String, Array],
|
||||||
|
desc: '要更新的支持https的负载均衡的域名'
|
||||||
|
},
|
||||||
|
loadBalancerId: {
|
||||||
|
label: '负载均衡ID',
|
||||||
|
desc: '如果没有配置,则根据域名匹配负载均衡下的监听器(根据域名匹配时暂时只支持前100个)'
|
||||||
|
},
|
||||||
|
listenerId: {
|
||||||
|
label: '监听器ID',
|
||||||
|
desc: '如果没有配置,则根据域名或负载均衡id匹配监听器'
|
||||||
|
},
|
||||||
|
certName: {
|
||||||
|
label: '证书名称',
|
||||||
|
desc: '如无uploadCertToTencent作为前置,则此项需要设置,默认为域名'
|
||||||
|
},
|
||||||
|
accessProvider: {
|
||||||
|
label: 'Access提供者',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
|
||||||
|
options: 'accessProviders[type=tencent]',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||||
|
const { region } = props
|
||||||
|
const client = this.getClient(accessProvider, region)
|
||||||
|
|
||||||
|
const lastCertId = await this.getCertIdFromProps(client, props)
|
||||||
|
if (!props.domain) {
|
||||||
|
await this.updateListener(client, cert, props, context)
|
||||||
|
} else {
|
||||||
|
await this.updateByDomainAttr(client, cert, props, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.sleep(2000)
|
||||||
|
let newCertId = await this.getCertIdFromProps(client, props)
|
||||||
|
if ((lastCertId && newCertId === lastCertId) || (!lastCertId && !newCertId)) {
|
||||||
|
await this.sleep(2000)
|
||||||
|
newCertId = await this.getCertIdFromProps(client, props)
|
||||||
|
}
|
||||||
|
if (newCertId === lastCertId) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
this.logger.info('腾讯云证书ID:', newCertId)
|
||||||
|
if (!context.tencentCertId) {
|
||||||
|
context.tencentCertId = newCertId
|
||||||
|
}
|
||||||
|
return { tencentCertId: newCertId }
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn('查询腾讯云证书失败', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCertIdFromProps (client, props) {
|
||||||
|
const listenerRet = await this.getListenerList(client, props.loadBalancerId, [props.listenerId])
|
||||||
|
return this.getCertIdFromListener(listenerRet[0], props.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
getCertIdFromListener (listener, domain) {
|
||||||
|
let certId
|
||||||
|
if (!domain) {
|
||||||
|
certId = listener.Certificate.CertId
|
||||||
|
} else {
|
||||||
|
if (listener.Rules && listener.Rules.length > 0) {
|
||||||
|
for (const rule of listener.Rules) {
|
||||||
|
if (rule.Domain === domain) {
|
||||||
|
if (rule.Certificate != null) {
|
||||||
|
certId = rule.Certificate.CertId
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return certId
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback ({ cert, props, context }) {
|
||||||
|
this.logger.warn('未实现rollback')
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateListener (client, cert, props, context) {
|
||||||
|
const params = this.buildProps(props, context, cert)
|
||||||
|
const ret = await client.ModifyListener(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('设置腾讯云CLB证书成功:', ret.RequestId, '->loadBalancerId:', props.loadBalancerId, 'listenerId', props.listenerId)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateByDomainAttr (client, cert, props, context) {
|
||||||
|
const params = this.buildProps(props, context, cert)
|
||||||
|
params.Domain = props.domain
|
||||||
|
const ret = await client.ModifyDomainAttributes(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('设置腾讯云CLB证书(sni)成功:', ret.RequestId, '->loadBalancerId:', props.loadBalancerId, 'listenerId', props.listenerId, 'domain:', props.domain)
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
buildProps (props, context, cert) {
|
||||||
|
const { certName } = props
|
||||||
|
const { tencentCertId } = context
|
||||||
|
this.logger.info('部署腾讯云证书ID:', tencentCertId)
|
||||||
|
const params = {
|
||||||
|
Certificate: {
|
||||||
|
SSLMode: 'UNIDIRECTIONAL', // 单向认证
|
||||||
|
CertId: tencentCertId
|
||||||
|
},
|
||||||
|
LoadBalancerId: props.loadBalancerId,
|
||||||
|
ListenerId: props.listenerId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tencentCertId == null) {
|
||||||
|
params.Certificate.CertName = this.appendTimeSuffix(certName || cert.domain)
|
||||||
|
params.Certificate.CertKey = cert.key
|
||||||
|
params.Certificate.CertContent = cert.crt
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCLBList (client, props) {
|
||||||
|
const params = {
|
||||||
|
Limit: 100, // 最大暂时只支持100个,暂时没做翻页
|
||||||
|
OrderBy: 'CreateTime',
|
||||||
|
OrderType: 0,
|
||||||
|
...props.DescribeLoadBalancers
|
||||||
|
}
|
||||||
|
const ret = await client.DescribeLoadBalancers(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
return ret.LoadBalancerSet
|
||||||
|
}
|
||||||
|
|
||||||
|
async getListenerList (client, balancerId, listenerIds) {
|
||||||
|
// HTTPS
|
||||||
|
const params = {
|
||||||
|
LoadBalancerId: balancerId,
|
||||||
|
Protocol: 'HTTPS',
|
||||||
|
ListenerIds: listenerIds
|
||||||
|
}
|
||||||
|
const ret = await client.DescribeListeners(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
return ret.Listeners
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient (accessProvider, region) {
|
||||||
|
const ClbClient = tencentcloud.clb.v20180317.Client
|
||||||
|
|
||||||
|
const clientConfig = {
|
||||||
|
credential: {
|
||||||
|
secretId: accessProvider.secretId,
|
||||||
|
secretKey: accessProvider.secretKey
|
||||||
|
},
|
||||||
|
region: region,
|
||||||
|
profile: {
|
||||||
|
httpProfile: {
|
||||||
|
endpoint: 'clb.tencentcloudapi.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClbClient(clientConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
170
packages/plugins/src/tencent/deploy-to-tke-ingress/index.js
Normal file
170
packages/plugins/src/tencent/deploy-to-tke-ingress/index.js
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { AbstractTencentPlugin } from '../../tencent/abstract-tencent.js'
|
||||||
|
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||||
|
import { K8sClient } from '../../utils/util.k8s.client.js'
|
||||||
|
export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'deployCertToTencentTKEIngress',
|
||||||
|
label: '部署到腾讯云TKE-ingress',
|
||||||
|
desc: '需要【上传到腾讯云】作为前置任务',
|
||||||
|
input: {
|
||||||
|
region: {
|
||||||
|
label: '大区',
|
||||||
|
value: 'ap-guangzhou'
|
||||||
|
},
|
||||||
|
clusterId: {
|
||||||
|
label: '集群ID',
|
||||||
|
required: true,
|
||||||
|
desc: '例如:cls-6lbj1vee'
|
||||||
|
},
|
||||||
|
namespace: {
|
||||||
|
label: '集群的namespace',
|
||||||
|
value: 'default'
|
||||||
|
},
|
||||||
|
secreteName: {
|
||||||
|
type: [String, Array],
|
||||||
|
label: '证书的secret名称',
|
||||||
|
desc: '支持多个(传入数组)'
|
||||||
|
},
|
||||||
|
ingressName: {
|
||||||
|
type: [String, Array],
|
||||||
|
label: 'ingress名称',
|
||||||
|
desc: '支持多个(传入数组)'
|
||||||
|
},
|
||||||
|
clusterIp: {
|
||||||
|
type: String,
|
||||||
|
label: '集群内网ip',
|
||||||
|
desc: '如果开启了外网的话,无需设置'
|
||||||
|
},
|
||||||
|
clusterDomain: {
|
||||||
|
type: String,
|
||||||
|
label: '集群域名,可不填,默认为:[clusterId].ccs.tencent-cloud.com'
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* AccessProvider的key,或者一个包含access的具体的对象
|
||||||
|
*/
|
||||||
|
accessProvider: {
|
||||||
|
label: 'Access提供者',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: '请选择access提供者',
|
||||||
|
component: {
|
||||||
|
name: 'accessProviderSelect',
|
||||||
|
props: {
|
||||||
|
filterType: 'tencent'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context }) {
|
||||||
|
const accessProvider = this.getAccessProvider(props.accessProvider)
|
||||||
|
const tkeClient = this.getTkeClient(accessProvider, props.region)
|
||||||
|
const kubeConfigStr = await this.getTkeKubeConfig(tkeClient, props.clusterId)
|
||||||
|
|
||||||
|
this.logger.info('kubeconfig已成功获取')
|
||||||
|
const k8sClient = new K8sClient(kubeConfigStr)
|
||||||
|
if (props.clusterIp != null) {
|
||||||
|
let clusterDomain = props.clusterDomain
|
||||||
|
if (!clusterDomain) {
|
||||||
|
clusterDomain = `${props.clusterId}.ccs.tencent-cloud.com`
|
||||||
|
}
|
||||||
|
// 修改内网解析ip地址
|
||||||
|
k8sClient.setLookup({ [clusterDomain]: { ip: props.clusterIp } })
|
||||||
|
}
|
||||||
|
await this.patchCertSecret({ k8sClient, props, context })
|
||||||
|
await this.sleep(2000) // 停留2秒,等待secret部署完成
|
||||||
|
await this.restartIngress({ k8sClient, props })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
getTkeClient (accessProvider, region = 'ap-guangzhou') {
|
||||||
|
const TkeClient = tencentcloud.tke.v20180525.Client
|
||||||
|
const clientConfig = {
|
||||||
|
credential: {
|
||||||
|
secretId: accessProvider.secretId,
|
||||||
|
secretKey: accessProvider.secretKey
|
||||||
|
},
|
||||||
|
region,
|
||||||
|
profile: {
|
||||||
|
httpProfile: {
|
||||||
|
endpoint: 'tke.tencentcloudapi.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TkeClient(clientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTkeKubeConfig (client, clusterId) {
|
||||||
|
// Depends on tencentcloud-sdk-nodejs version 4.0.3 or higher
|
||||||
|
const params = {
|
||||||
|
ClusterId: clusterId
|
||||||
|
}
|
||||||
|
const ret = await client.DescribeClusterKubeconfig(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('注意:后续操作需要在【集群->基本信息】中开启外网或内网访问,https://console.cloud.tencent.com/tke2/cluster')
|
||||||
|
return ret.Kubeconfig
|
||||||
|
}
|
||||||
|
|
||||||
|
async patchCertSecret ({ k8sClient, props, context }) {
|
||||||
|
const { tencentCertId } = context
|
||||||
|
if (tencentCertId == null) {
|
||||||
|
throw new Error('请先将【上传证书到腾讯云】作为前置任务')
|
||||||
|
}
|
||||||
|
this.logger.info('腾讯云证书ID:', tencentCertId)
|
||||||
|
const certIdBase64 = Buffer.from(tencentCertId).toString('base64')
|
||||||
|
|
||||||
|
const { namespace, secretName } = props
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
data: {
|
||||||
|
qcloud_cert_id: certIdBase64
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
certd: this.appendTimeSuffix('certd')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let secretNames = secretName
|
||||||
|
if (typeof secretName === 'string') {
|
||||||
|
secretNames = [secretName]
|
||||||
|
}
|
||||||
|
for (const secret of secretNames) {
|
||||||
|
await k8sClient.patchSecret({ namespace, secretName: secret, body })
|
||||||
|
this.logger.info(`CertSecret已更新:${secret}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async restartIngress ({ k8sClient, props }) {
|
||||||
|
const { namespace, ingressName } = props
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
metadata: {
|
||||||
|
labels: {
|
||||||
|
certd: this.appendTimeSuffix('certd')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ingressNames = ingressName
|
||||||
|
if (typeof ingressName === 'string') {
|
||||||
|
ingressNames = [ingressName]
|
||||||
|
}
|
||||||
|
for (const ingress of ingressNames) {
|
||||||
|
await k8sClient.patchIngress({ namespace, ingressName: ingress, body })
|
||||||
|
this.logger.info(`ingress已重启:${ingress}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
87
packages/plugins/src/tencent/upload-to-tencent/index.js
Normal file
87
packages/plugins/src/tencent/upload-to-tencent/index.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import dayjs from 'dayjs'
|
||||||
|
import tencentcloud from 'tencentcloud-sdk-nodejs'
|
||||||
|
import { AbstractTencentPlugin } from '../abstract-tencent.js'
|
||||||
|
|
||||||
|
export class UploadCertToTencent extends AbstractTencentPlugin {
|
||||||
|
/**
|
||||||
|
* 插件定义
|
||||||
|
* 名称
|
||||||
|
* 入参
|
||||||
|
* 出参
|
||||||
|
*/
|
||||||
|
static define () {
|
||||||
|
return {
|
||||||
|
name: 'uploadCertToTencent',
|
||||||
|
label: '上传证书到腾讯云',
|
||||||
|
input: {
|
||||||
|
name: {
|
||||||
|
label: '证书名称'
|
||||||
|
},
|
||||||
|
accessProvider: {
|
||||||
|
label: 'Access提供者',
|
||||||
|
type: [String, Object],
|
||||||
|
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
|
||||||
|
options: 'accessProviders[type=tencent]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
tencentCertId: {
|
||||||
|
type: String,
|
||||||
|
desc: '上传成功后的腾讯云CertId'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient (accessProvider) {
|
||||||
|
const SslClient = tencentcloud.ssl.v20191205.Client
|
||||||
|
|
||||||
|
const clientConfig = {
|
||||||
|
credential: {
|
||||||
|
secretId: accessProvider.secretId,
|
||||||
|
secretKey: accessProvider.secretKey
|
||||||
|
},
|
||||||
|
region: '',
|
||||||
|
profile: {
|
||||||
|
httpProfile: {
|
||||||
|
endpoint: 'ssl.tencentcloudapi.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SslClient(clientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute ({ cert, props, context, logger }) {
|
||||||
|
const { name, accessProvider } = props
|
||||||
|
const certName = this.appendTimeSuffix(name || cert.domain)
|
||||||
|
|
||||||
|
const provider = this.getAccessProvider(accessProvider)
|
||||||
|
const client = this.getClient(provider)
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
CertificatePublicKey: cert.crt,
|
||||||
|
CertificatePrivateKey: cert.key,
|
||||||
|
Alias: certName
|
||||||
|
}
|
||||||
|
const ret = await client.UploadCertificate(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('证书上传成功:tencentCertId=', ret.CertificateId)
|
||||||
|
context.tencentCertId = ret.CertificateId
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback ({ cert, props, context }) {
|
||||||
|
const { accessProvider } = props
|
||||||
|
const provider = super.getAccessProvider(accessProvider)
|
||||||
|
const client = this.getClient(provider)
|
||||||
|
|
||||||
|
const { tencentCertId } = context
|
||||||
|
const params = {
|
||||||
|
CertificateId: tencentCertId
|
||||||
|
}
|
||||||
|
const ret = await client.DeleteCertificate(params)
|
||||||
|
this.checkRet(ret)
|
||||||
|
this.logger.info('证书删除成功:DeleteResult=', ret.DeleteResult)
|
||||||
|
delete context.tencentCertId
|
||||||
|
}
|
||||||
|
}
|
||||||
109
packages/plugins/src/utils/util.k8s.client.js
Normal file
109
packages/plugins/src/utils/util.k8s.client.js
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import kubernetesClient from 'kubernetes-client'
|
||||||
|
import { util } from '@certd/api'
|
||||||
|
import Request from 'kubernetes-client/backends/request/index.js'
|
||||||
|
import dns from 'dns'
|
||||||
|
const { KubeConfig, Client } = kubernetesClient
|
||||||
|
const logger = util.logger
|
||||||
|
|
||||||
|
export class K8sClient {
|
||||||
|
constructor (kubeConfigStr) {
|
||||||
|
this.kubeConfigStr = kubeConfigStr
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
init () {
|
||||||
|
const kubeconfig = new KubeConfig()
|
||||||
|
kubeconfig.loadFromString(this.kubeConfigStr)
|
||||||
|
const reqOpts = { kubeconfig, request: {} }
|
||||||
|
if (this.lookup) {
|
||||||
|
reqOpts.request.lookup = this.lookup
|
||||||
|
}
|
||||||
|
|
||||||
|
const backend = new Request(reqOpts)
|
||||||
|
this.client = new Client({ backend, version: '1.13' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param localRecords { [domain]:{ip:'xxx.xx.xxx'} }
|
||||||
|
*/
|
||||||
|
setLookup (localRecords) {
|
||||||
|
this.lookup = (hostnameReq, options, callback) => {
|
||||||
|
logger.info('custom lookup', hostnameReq, localRecords)
|
||||||
|
if (localRecords[hostnameReq]) {
|
||||||
|
logger.info('local record', hostnameReq, localRecords[hostnameReq])
|
||||||
|
callback(null, localRecords[hostnameReq].ip, 4)
|
||||||
|
} else {
|
||||||
|
dns.lookup(hostnameReq, options, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询 secret列表
|
||||||
|
* @param opts = {namespace:default}
|
||||||
|
* @returns secretsList
|
||||||
|
*/
|
||||||
|
async getSecret (opts) {
|
||||||
|
const namespace = opts.namespace || 'default'
|
||||||
|
const secrets = await this.client.api.v1.namespaces(namespace).secrets.get()
|
||||||
|
return secrets
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建Secret
|
||||||
|
* @param opts {namespace:default, body:yamlStr}
|
||||||
|
* @returns {Promise<*>}
|
||||||
|
*/
|
||||||
|
async createSecret (opts) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSecret (opts) {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async patchSecret (opts) {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getIngress (opts) {
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
21
packages/plugins/test/aliyun/deploy-to-cdn.test.js
Normal file
21
packages/plugins/test/aliyun/deploy-to-cdn.test.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { DeployCertToAliyunCDN } from '../../src/aliyun/deploy-to-cdn/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import createOptions from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('DeployToAliyunCDN', function () {
|
||||||
|
it('#execute', async function () {
|
||||||
|
this.timeout(5000)
|
||||||
|
const options = createOptions()
|
||||||
|
const plugin = new DeployCertToAliyunCDN()
|
||||||
|
options.cert.domains = ['*.docmirror.cn', 'docmirror.cn']
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const ret = await plugin.doExecute({
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: { domainName: 'certd-cdn-upload.docmirror.cn', certName: 'certd部署测试', certType: 'cas', accessProvider: 'aliyun' }
|
||||||
|
})
|
||||||
|
console.log('context:', context, ret)
|
||||||
|
})
|
||||||
|
})
|
||||||
28
packages/plugins/test/aliyun/upload-to-aliyun.test.js
Normal file
28
packages/plugins/test/aliyun/upload-to-aliyun.test.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { UploadCertToAliyun } from '../../src/aliyun/upload-to-aliyun/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('PluginUploadToAliyun', function () {
|
||||||
|
it('#execute', async function () {
|
||||||
|
this.timeout(5000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['_.docmirror.cn']
|
||||||
|
const plugin = new UploadCertToAliyun()
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const context = {}
|
||||||
|
const deployOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: { accessProvider: 'aliyun' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
await plugin.doExecute(deployOpts)
|
||||||
|
console.log('context:', context)
|
||||||
|
|
||||||
|
// await plugin.sleep(1000)
|
||||||
|
// await plugin.rollback(deployOpts)
|
||||||
|
})
|
||||||
|
})
|
||||||
29
packages/plugins/test/host/host-shell-execute.test.js
Normal file
29
packages/plugins/test/host/host-shell-execute.test.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { HostShellExecute } from '../../src/host/host-shell-execute/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('HostShellExecute', function () {
|
||||||
|
it('#execute', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.args = { test: false }
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.cn']
|
||||||
|
const plugin = new HostShellExecute(options)
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const context = {}
|
||||||
|
const uploadOpts = {
|
||||||
|
cert,
|
||||||
|
props: { script: 'ls ', accessProvider: 'aliyun-ssh' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
const ret = await plugin.doExecute(uploadOpts)
|
||||||
|
for (const retElement of ret) {
|
||||||
|
console.log('-----' + retElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
await plugin.doRollback(uploadOpts)
|
||||||
|
})
|
||||||
|
})
|
||||||
27
packages/plugins/test/host/upload-to-host.test.js
Normal file
27
packages/plugins/test/host/upload-to-host.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { UploadCertToHost } from '../../src/host/upload-to-host/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('PluginUploadToHost', function () {
|
||||||
|
it('#execute', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.args = { test: false }
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.cn']
|
||||||
|
const plugin = new UploadCertToHost(options)
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const context = {}
|
||||||
|
const uploadOpts = {
|
||||||
|
cert,
|
||||||
|
props: { crtPath: '/root/certd/test/test.crt', keyPath: '/root/certd/test/test.key', accessProvider: 'aliyun-ssh' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
await plugin.doExecute(uploadOpts)
|
||||||
|
console.log('context:', context)
|
||||||
|
|
||||||
|
await plugin.doRollback(uploadOpts)
|
||||||
|
})
|
||||||
|
})
|
||||||
42
packages/plugins/test/options.js
Normal file
42
packages/plugins/test/options.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import _ from 'lodash-es'
|
||||||
|
import optionsPrivate from '../../../test/options.private.mjs'
|
||||||
|
const defaultOptions = {
|
||||||
|
version: '1.0.0',
|
||||||
|
args: {
|
||||||
|
directory: 'test',
|
||||||
|
dry: false
|
||||||
|
},
|
||||||
|
accessProviders: {
|
||||||
|
aliyun: {
|
||||||
|
providerType: 'aliyun',
|
||||||
|
accessKeyId: '',
|
||||||
|
accessKeySecret: ''
|
||||||
|
},
|
||||||
|
myLinux: {
|
||||||
|
providerType: 'SSH',
|
||||||
|
username: 'xxx',
|
||||||
|
password: 'xxx',
|
||||||
|
host: '1111.com',
|
||||||
|
port: 22,
|
||||||
|
publicKey: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cert: {
|
||||||
|
domains: ['*.docmirror.club', 'docmirror.club'],
|
||||||
|
email: 'xiaojunnuo@qq.com',
|
||||||
|
dnsProvider: 'aliyun',
|
||||||
|
certProvider: 'letsencrypt',
|
||||||
|
csrInfo: {
|
||||||
|
country: 'CN',
|
||||||
|
state: 'GuangDong',
|
||||||
|
locality: 'ShengZhen',
|
||||||
|
organization: 'CertD Org.',
|
||||||
|
organizationUnit: 'IT Department',
|
||||||
|
emailAddress: 'xiaojunnuo@qq.com'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_.merge(defaultOptions, optionsPrivate)
|
||||||
|
|
||||||
|
export default defaultOptions
|
||||||
54
packages/plugins/test/tencent/deploy-to-cdn.test.js
Normal file
54
packages/plugins/test/tencent/deploy-to-cdn.test.js
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { DeployCertToTencentCDN } from '../../src/tencent/deploy-to-cdn/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import { UploadCertToTencent } from '../../src/tencent/upload-to-tencent/index.js'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('DeployToTencentCDN', function () {
|
||||||
|
it('#execute-from-store', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
|
||||||
|
const context = {}
|
||||||
|
const uploadPlugin = new UploadCertToTencent()
|
||||||
|
const uploadOptions = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: { name: 'certd部署测试', accessProvider: 'tencent' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
await uploadPlugin.doExecute(uploadOptions)
|
||||||
|
|
||||||
|
const deployPlugin = new DeployCertToTencentCDN()
|
||||||
|
const deployOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: { domainName: 'tentcent-certd.docmirror.cn', certName: 'certd部署测试', accessProvider: 'tencent' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
const ret = await deployPlugin.doExecute(deployOpts)
|
||||||
|
expect(ret).ok
|
||||||
|
console.log('context:', context)
|
||||||
|
|
||||||
|
await uploadPlugin.doRollback(uploadOptions)
|
||||||
|
})
|
||||||
|
it('#execute-upload', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.cn']
|
||||||
|
const plugin = new DeployCertToTencentCDN()
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const context = {}
|
||||||
|
const deployOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: { domainName: 'tentcent-certd.docmirror.cn', accessProvider: 'tencent' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
const ret = await plugin.doExecute(deployOpts)
|
||||||
|
console.log('context:', context, ret)
|
||||||
|
})
|
||||||
|
})
|
||||||
107
packages/plugins/test/tencent/deploy-to-clb.test.js
Normal file
107
packages/plugins/test/tencent/deploy-to-clb.test.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { DeployCertToTencentCLB } from '../../src/tencent/deploy-to-clb/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
import { UploadCertToTencent } from '../../src/tencent/upload-to-tencent/index.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('DeployToTencentCLB', function () {
|
||||||
|
it('#execute-getClbList', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
options.cert.dnsProvider = 'tencent-yonsz'
|
||||||
|
const deployPlugin = new DeployCertToTencentCLB()
|
||||||
|
const props = {
|
||||||
|
region: 'ap-guangzhou',
|
||||||
|
domain: 'certd-test-no-sni.base.yonsz.net',
|
||||||
|
accessProvider: 'tencent-yonsz'
|
||||||
|
}
|
||||||
|
const accessProviders = options.accessProviders
|
||||||
|
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider, accessProviders)
|
||||||
|
const { region } = props
|
||||||
|
const client = deployPlugin.getClient(accessProvider, region)
|
||||||
|
|
||||||
|
const ret = await deployPlugin.getCLBList(client, props)
|
||||||
|
expect(ret.length > 0).ok
|
||||||
|
console.log('clb count:', ret.length)
|
||||||
|
})
|
||||||
|
it('#execute-getListenerList', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
options.cert.dnsProvider = 'tencent-yonsz'
|
||||||
|
const deployPlugin = new DeployCertToTencentCLB(options)
|
||||||
|
const props = {
|
||||||
|
region: 'ap-guangzhou',
|
||||||
|
domain: 'certd-test-no-sni.base.yonsz.net',
|
||||||
|
accessProvider: 'tencent-yonsz',
|
||||||
|
loadBalancerId: 'lb-59yhe5xo'
|
||||||
|
}
|
||||||
|
const accessProvider = deployPlugin.getAccessProvider(props.accessProvider)
|
||||||
|
const { region } = props
|
||||||
|
const client = deployPlugin.getClient(accessProvider, region)
|
||||||
|
|
||||||
|
const ret = await deployPlugin.getListenerList(client, props.loadBalancerId, props)
|
||||||
|
expect(ret.length > 0).ok
|
||||||
|
console.log('clb count:', ret.length, ret)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('#execute-no-sni-listenerId', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
options.cert.dnsProvider = 'tencent-yonsz'
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.cn']
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const deployPlugin = new DeployCertToTencentCLB()
|
||||||
|
const context = {}
|
||||||
|
const deployOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: {
|
||||||
|
region: 'ap-guangzhou',
|
||||||
|
loadBalancerId: 'lb-59yhe5xo',
|
||||||
|
listenerId: 'lbl-1vfwx8dq',
|
||||||
|
accessProvider: 'tencent-yonsz'
|
||||||
|
},
|
||||||
|
context
|
||||||
|
}
|
||||||
|
const ret = await deployPlugin.doExecute(deployOpts)
|
||||||
|
expect(ret).ok
|
||||||
|
console.log('ret:', ret)
|
||||||
|
|
||||||
|
// 删除测试证书
|
||||||
|
const uploadPlugin = new UploadCertToTencent()
|
||||||
|
await uploadPlugin.doRollback(deployOpts)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('#execute-sni-listenerId', async function () {
|
||||||
|
this.timeout(10000)
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
options.cert.dnsProvider = 'tencent-yonsz'
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = certd.readCurrentCert('xiaojunnuo@qq.com', ['*.docmirror.cn'])
|
||||||
|
const deployPlugin = new DeployCertToTencentCLB()
|
||||||
|
const context = {}
|
||||||
|
const deployOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: {
|
||||||
|
region: 'ap-guangzhou',
|
||||||
|
loadBalancerId: 'lb-59yhe5xo',
|
||||||
|
listenerId: 'lbl-akbyf5ac',
|
||||||
|
domain: 'certd-test-sni.base.yonsz.net',
|
||||||
|
accessProvider: 'tencent-yonsz'
|
||||||
|
},
|
||||||
|
context
|
||||||
|
}
|
||||||
|
const ret = await deployPlugin.doExecute(deployOpts)
|
||||||
|
expect(ret).ok
|
||||||
|
console.log('ret:', ret)
|
||||||
|
// 删除测试证书
|
||||||
|
const uploadPlugin = new UploadCertToTencent()
|
||||||
|
await uploadPlugin.doRollback(deployOpts)
|
||||||
|
})
|
||||||
|
})
|
||||||
114
packages/plugins/test/tencent/deploy-to-tke-ingress.test.js
Normal file
114
packages/plugins/test/tencent/deploy-to-tke-ingress.test.js
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { DeployCertToTencentTKEIngress } from '../../src/tencent/deploy-to-tke-ingress/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
import { K8sClient } from '../../src/utils/util.k8s.client.js'
|
||||||
|
|
||||||
|
const { expect } = pkg
|
||||||
|
|
||||||
|
async function getOptions () {
|
||||||
|
const options = createOptions()
|
||||||
|
options.args.test = false
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.cn']
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const context = {}
|
||||||
|
const deployOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: {
|
||||||
|
accessProvider: 'tencent-yonsz',
|
||||||
|
region: 'ap-guangzhou',
|
||||||
|
clusterId: 'cls-6lbj1vee'
|
||||||
|
},
|
||||||
|
context
|
||||||
|
}
|
||||||
|
return { options, deployOpts }
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DeployCertToTencentTKEIngress', function () {
|
||||||
|
// it('#getTkeKubeConfig', async function () {
|
||||||
|
// const { options, deployOpts } = await getOptions()
|
||||||
|
// const plugin = new DeployCertToTencentTKEIngress()
|
||||||
|
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||||
|
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
|
||||||
|
// console.log('kubeConfig:', kubeConfig)
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
it('#getTKESecrets', async function () {
|
||||||
|
this.timeout(50000)
|
||||||
|
const { options, deployOpts } = await getOptions()
|
||||||
|
const plugin = new DeployCertToTencentTKEIngress(options)
|
||||||
|
const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||||
|
const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props.clusterId)
|
||||||
|
|
||||||
|
const k8sClient = new K8sClient(kubeConfig)
|
||||||
|
k8sClient.setLookup({
|
||||||
|
'cls-6lbj1vee.ccs.tencent-cloud.com': { ip: '13.123.123.123' }
|
||||||
|
})
|
||||||
|
const secrets = await k8sClient.getSecret()
|
||||||
|
|
||||||
|
console.log('secrets:', secrets)
|
||||||
|
})
|
||||||
|
//
|
||||||
|
// it('#patchTKECertSecrets', async function () {
|
||||||
|
// this.timeout(5000)
|
||||||
|
//
|
||||||
|
// const { options, deployOpts } = await getOptions()
|
||||||
|
// const plugin = new DeployCertToTencentTKEIngress()
|
||||||
|
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||||
|
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
|
||||||
|
// const k8sClient = new K8sClient(kubeConfig)
|
||||||
|
//
|
||||||
|
// deployOpts.k8sClient = k8sClient
|
||||||
|
// deployOpts.context.tencentCertId = 'hNVD3Z45'
|
||||||
|
// const newCecret = await plugin.patchCertSecret(deployOpts)
|
||||||
|
// console.log('newCecret', newCecret)
|
||||||
|
// })
|
||||||
|
// it('#GetTkeIngress', async function () {
|
||||||
|
// this.timeout(5000)
|
||||||
|
//
|
||||||
|
// const { options, deployOpts } = await getOptions()
|
||||||
|
// deployOpts.props.ingressName = 'ingress-base'
|
||||||
|
// deployOpts.props.secretName = 'cert---docmirror-cn'
|
||||||
|
// const plugin = new DeployCertToTencentTKEIngress()
|
||||||
|
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||||
|
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
|
||||||
|
//
|
||||||
|
// const k8sClient = new K8sClient(kubeConfig)
|
||||||
|
// const ingress = await k8sClient.getIngress({
|
||||||
|
// ingressName: 'ingress-base'
|
||||||
|
// })
|
||||||
|
// console.log('ingress:', ingress)
|
||||||
|
// })
|
||||||
|
// it('#RestartTKEIngress', async function () {
|
||||||
|
// this.timeout(5000)
|
||||||
|
//
|
||||||
|
// const { options, deployOpts } = await getOptions()
|
||||||
|
// deployOpts.props.ingressName = 'ingress-base'
|
||||||
|
// deployOpts.props.secretName = 'cert---docmirror-cn'
|
||||||
|
// const plugin = new DeployCertToTencentTKEIngress()
|
||||||
|
// const tkeClient = plugin.getTkeClient(options.accessProviders[deployOpts.props.accessProvider], deployOpts.props.region)
|
||||||
|
// const kubeConfig = await plugin.getTkeKubeConfig(tkeClient, deployOpts.props)
|
||||||
|
//
|
||||||
|
// const k8sClient = new K8sClient(kubeConfig)
|
||||||
|
//
|
||||||
|
// deployOpts.k8sClient = k8sClient
|
||||||
|
// deployOpts.context.tencentCertId = 'hNVD3Z45'
|
||||||
|
// const newCecret = await plugin.restartIngress(deployOpts)
|
||||||
|
// console.log('newCecret', newCecret)
|
||||||
|
// })
|
||||||
|
|
||||||
|
it('#execute', async function () {
|
||||||
|
this.timeout(5000)
|
||||||
|
const { deployOpts } = await getOptions()
|
||||||
|
deployOpts.props.ingressName = 'ingress-base'
|
||||||
|
deployOpts.props.secretName = 'cert---docmirror-cn'
|
||||||
|
deployOpts.context.tencentCertId = 'hNUZJrZf'
|
||||||
|
const plugin = new DeployCertToTencentTKEIngress()
|
||||||
|
|
||||||
|
const ret = await plugin.doExecute(deployOpts)
|
||||||
|
console.log('sucess', ret)
|
||||||
|
})
|
||||||
|
})
|
||||||
27
packages/plugins/test/tencent/upload-to-tencent.test.js
Normal file
27
packages/plugins/test/tencent/upload-to-tencent.test.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import { UploadCertToTencent } from '../../src/tencent/upload-to-tencent/index.js'
|
||||||
|
import { Certd } from '@certd/certd'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('PluginUploadToTencent', function () {
|
||||||
|
it('#execute', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
const plugin = new UploadCertToTencent()
|
||||||
|
options.args = { test: false }
|
||||||
|
options.cert.email = 'xiaojunnuo@qq.com'
|
||||||
|
options.cert.domains = ['*.docmirror.cn']
|
||||||
|
const certd = new Certd(options)
|
||||||
|
const cert = await certd.readCurrentCert()
|
||||||
|
const context = {}
|
||||||
|
const uploadOpts = {
|
||||||
|
accessProviders: options.accessProviders,
|
||||||
|
cert,
|
||||||
|
props: { name: 'certd部署测试', accessProvider: 'tencent' },
|
||||||
|
context
|
||||||
|
}
|
||||||
|
await plugin.doExecute(uploadOpts)
|
||||||
|
console.log('context:', context)
|
||||||
|
|
||||||
|
await plugin.doRollback(uploadOpts)
|
||||||
|
})
|
||||||
|
})
|
||||||
1858
packages/plugins/yarn-error.log
Normal file
1858
packages/plugins/yarn-error.log
Normal file
File diff suppressed because it is too large
Load Diff
2922
packages/plugins/yarn.lock
Normal file
2922
packages/plugins/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
14
packages/providers/.eslintrc
Normal file
14
packages/providers/.eslintrc
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"extends": "standard",
|
||||||
|
"env": {
|
||||||
|
"mocha": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.test.js", "*.spec.js"],
|
||||||
|
"rules": {
|
||||||
|
"no-unused-expressions": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
2814
packages/providers/package-lock.json
generated
Normal file
2814
packages/providers/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
packages/providers/package.json
Normal file
27
packages/providers/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "@certd/providers",
|
||||||
|
"version": "0.1.11",
|
||||||
|
"description": "",
|
||||||
|
"main": "./src/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"author": "Greper",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@alicloud/pop-core": "^1.7.10",
|
||||||
|
"@certd/api": "^0.1.11",
|
||||||
|
"lodash-es": "^4.17.20",
|
||||||
|
"tencentcloud-sdk-nodejs": "^4.0.44"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "^4.2.0",
|
||||||
|
"eslint": "^7.15.0",
|
||||||
|
"eslint-config-standard": "^16.0.2",
|
||||||
|
"eslint-plugin-import": "^2.22.1",
|
||||||
|
"eslint-plugin-node": "^11.1.0",
|
||||||
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
|
"mocha": "^8.2.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
110
packages/providers/src/dns-provider/aliyun.js
Normal file
110
packages/providers/src/dns-provider/aliyun.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { AbstractDnsProvider } from '@certd/api'
|
||||||
|
import Core from '@alicloud/pop-core'
|
||||||
|
import _ from 'lodash-es'
|
||||||
|
export class AliyunDnsProvider extends AbstractDnsProvider {
|
||||||
|
constructor (dnsProviderConfig) {
|
||||||
|
super()
|
||||||
|
this.client = new Core({
|
||||||
|
accessKeyId: dnsProviderConfig.accessKeyId,
|
||||||
|
accessKeySecret: dnsProviderConfig.accessKeySecret,
|
||||||
|
endpoint: 'https://alidns.aliyuncs.com',
|
||||||
|
apiVersion: '2015-01-09'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static name () {
|
||||||
|
return 'aliyun'
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDomainList () {
|
||||||
|
const params = {
|
||||||
|
RegionId: 'cn-hangzhou'
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = await this.client.request('DescribeDomains', params, requestOption)
|
||||||
|
return ret.Domains.Domain
|
||||||
|
}
|
||||||
|
|
||||||
|
async matchDomain (dnsRecord) {
|
||||||
|
const list = await this.getDomainList()
|
||||||
|
let domain = null
|
||||||
|
for (const item of list) {
|
||||||
|
if (_.endsWith(dnsRecord, item.DomainName)) {
|
||||||
|
domain = item.DomainName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!domain) {
|
||||||
|
throw new Error('can not find Domain ,' + dnsRecord)
|
||||||
|
}
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRecords (domain, rr, value) {
|
||||||
|
const params = {
|
||||||
|
RegionId: 'cn-hangzhou',
|
||||||
|
DomainName: domain,
|
||||||
|
RRKeyWord: rr
|
||||||
|
}
|
||||||
|
if (value) {
|
||||||
|
params.ValueKeyWord = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = await this.client.request('DescribeDomainRecords', params, requestOption)
|
||||||
|
return ret.DomainRecords.Record
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRecord ({ fullRecord, type, value }) {
|
||||||
|
this.logger.info('添加域名解析:', fullRecord, value)
|
||||||
|
const domain = await this.matchDomain(fullRecord)
|
||||||
|
const rr = fullRecord.replace('.' + domain, '')
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
RegionId: 'cn-hangzhou',
|
||||||
|
DomainName: domain,
|
||||||
|
RR: rr,
|
||||||
|
Type: type,
|
||||||
|
Value: value
|
||||||
|
// Line: 'oversea' // 海外
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ret = await this.client.request('AddDomainRecord', params, requestOption)
|
||||||
|
this.logger.info('添加域名解析成功:', value, value, ret.RecordId)
|
||||||
|
return ret.RecordId
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'DomainRecordDuplicate') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.logger.info('添加域名解析出错', e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRecord ({ fullRecord, type, value, record }) {
|
||||||
|
const params = {
|
||||||
|
RegionId: 'cn-hangzhou',
|
||||||
|
RecordId: record
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestOption = {
|
||||||
|
method: 'POST'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = await this.client.request('DeleteDomainRecord', params, requestOption)
|
||||||
|
this.logger.info('删除域名解析成功:', fullRecord, value, ret.RecordId)
|
||||||
|
return ret.RecordId
|
||||||
|
}
|
||||||
|
}
|
||||||
79
packages/providers/src/dns-provider/dnspod.js
Normal file
79
packages/providers/src/dns-provider/dnspod.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { AbstractDnsProvider, util } from '@certd/api'
|
||||||
|
import _ from 'lodash-es'
|
||||||
|
const request = util.request
|
||||||
|
export class DnspodDnsProvider extends AbstractDnsProvider {
|
||||||
|
static name () {
|
||||||
|
return 'dnspod'
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor (dnsProviderConfig) {
|
||||||
|
super()
|
||||||
|
if (!dnsProviderConfig.id || !dnsProviderConfig.token) {
|
||||||
|
throw new Error('请正确配置dnspod的 id 和 token')
|
||||||
|
}
|
||||||
|
this.loginToken = dnsProviderConfig.id + ',' + dnsProviderConfig.token
|
||||||
|
}
|
||||||
|
|
||||||
|
async doRequest (options) {
|
||||||
|
const config = {
|
||||||
|
method: 'post',
|
||||||
|
formData: {
|
||||||
|
login_token: this.loginToken,
|
||||||
|
format: 'json',
|
||||||
|
lang: 'cn',
|
||||||
|
error_on_empty: 'no'
|
||||||
|
},
|
||||||
|
timeout: 5000
|
||||||
|
}
|
||||||
|
_.merge(config, options)
|
||||||
|
|
||||||
|
const ret = await request(config)
|
||||||
|
if (!ret || !ret.status || ret.status.code !== '1') {
|
||||||
|
throw new Error('请求失败:' + ret.status.message + ',api=' + config.url)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDomainList () {
|
||||||
|
const ret = await this.doRequest({
|
||||||
|
url: 'https://dnsapi.cn/Domain.List'
|
||||||
|
})
|
||||||
|
this.logger.debug('dnspod 域名列表:', ret.domains)
|
||||||
|
return ret.domains
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRecord ({ fullRecord, type, value }) {
|
||||||
|
this.logger.info('添加域名解析:', fullRecord, value)
|
||||||
|
const domainItem = await this.matchDomain(fullRecord, 'name')
|
||||||
|
const domain = domainItem.name
|
||||||
|
const rr = fullRecord.replace('.' + domain, '')
|
||||||
|
|
||||||
|
const ret = await this.doRequest({
|
||||||
|
url: 'https://dnsapi.cn/Record.Create',
|
||||||
|
formData: {
|
||||||
|
domain,
|
||||||
|
sub_domain: rr,
|
||||||
|
record_type: type,
|
||||||
|
record_line: '默认',
|
||||||
|
value: value,
|
||||||
|
mx: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.logger.info('添加域名解析成功:', fullRecord, value, JSON.stringify(ret.record))
|
||||||
|
return ret.record
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeRecord ({ fullRecord, type, value, record }) {
|
||||||
|
const domain = await this.matchDomain(fullRecord, 'name')
|
||||||
|
|
||||||
|
const ret = await this.doRequest({
|
||||||
|
url: 'https://dnsapi.cn/Record.Remove',
|
||||||
|
formData: {
|
||||||
|
domain,
|
||||||
|
record_id: record.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.logger.info('删除域名解析成功:', fullRecord, value)
|
||||||
|
return ret.RecordId
|
||||||
|
}
|
||||||
|
}
|
||||||
7
packages/providers/src/index.js
Normal file
7
packages/providers/src/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { AliyunDnsProvider } from './dns-provider/aliyun.js'
|
||||||
|
import { DnspodDnsProvider } from './dns-provider/dnspod.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
AliyunDnsProvider,
|
||||||
|
DnspodDnsProvider
|
||||||
|
}
|
||||||
33
packages/providers/test/dns-provider/aliyun.test.js
Normal file
33
packages/providers/test/dns-provider/aliyun.test.js
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import AliyunDnsProvider from '../../src/dns-provider/aliyun.js'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('AliyunDnsProvider', function () {
|
||||||
|
it('#getDomainList', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
|
||||||
|
const domainList = await aliyunDnsProvider.getDomainList()
|
||||||
|
console.log('domainList', domainList)
|
||||||
|
expect(domainList.length).gt(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('#getRecords', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
|
||||||
|
const recordList = await aliyunDnsProvider.getRecords('docmirror.cn', '*')
|
||||||
|
console.log('recordList', recordList)
|
||||||
|
expect(recordList.length).gt(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('#createAndRemoveRecord', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
const aliyunDnsProvider = new AliyunDnsProvider(options.accessProviders.aliyun)
|
||||||
|
const record = await aliyunDnsProvider.createRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa' })
|
||||||
|
console.log('recordId', record)
|
||||||
|
expect(record != null).ok
|
||||||
|
|
||||||
|
const recordId = await aliyunDnsProvider.removeRecord({ fullRecord: '___certd___.__test__.docmirror.cn', type: 'TXT', value: 'aaaa', record })
|
||||||
|
console.log('recordId', recordId)
|
||||||
|
expect(recordId != null).ok
|
||||||
|
})
|
||||||
|
})
|
||||||
24
packages/providers/test/dns-provider/dnspod.test.js
Normal file
24
packages/providers/test/dns-provider/dnspod.test.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import pkg from 'chai'
|
||||||
|
import DnspodDnsProvider from '../../src/dns-provider/dnspod.js'
|
||||||
|
import { Certd } from '../../src/index.js'
|
||||||
|
import { createOptions } from '../../../../test/options.js'
|
||||||
|
const { expect } = pkg
|
||||||
|
describe('DnspodDnsProvider', function () {
|
||||||
|
it('#getDomainList', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
|
||||||
|
const domainList = await dnsProvider.getDomainList()
|
||||||
|
console.log('domainList', domainList)
|
||||||
|
expect(domainList.length).gt(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('#createRecord&removeRecord', async function () {
|
||||||
|
const options = createOptions()
|
||||||
|
const dnsProvider = new DnspodDnsProvider(options.accessProviders.dnspod)
|
||||||
|
const record = await dnsProvider.createRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa' })
|
||||||
|
console.log('recordId', record.id)
|
||||||
|
expect(record.id != null).ok
|
||||||
|
|
||||||
|
await dnsProvider.removeRecord({ fullRecord: '___certd___.__test__.certd.xyz', type: 'TXT', value: 'aaaa', record })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
> 1%
|
|
||||||
last 2 versions
|
|
||||||
not dead
|
|
||||||
@@ -1,463 +0,0 @@
|
|||||||
/** @type {import('dependency-cruiser').IConfiguration} */
|
|
||||||
module.exports = {
|
|
||||||
forbidden: [
|
|
||||||
/* rules from the 'recommended' preset: */
|
|
||||||
{
|
|
||||||
name: 'no-circular',
|
|
||||||
severity: 'warn',
|
|
||||||
comment:
|
|
||||||
'This dependency is part of a circular relationship. You might want to revise ' +
|
|
||||||
'your solution (i.e. use dependency inversion, make sure the modules have a single responsibility) ',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
circular: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'no-orphans',
|
|
||||||
comment:
|
|
||||||
"This is an orphan module - it's likely not used (anymore?). Either use it or " +
|
|
||||||
"remove it. If it's logical this module is an orphan (i.e. it's a config file), " +
|
|
||||||
"add an exception for it in your dependency-cruiser configuration. By default " +
|
|
||||||
"this rule does not scrutinize dot-files (e.g. .eslintrc.js), TypeScript declaration " +
|
|
||||||
"files (.d.ts), tsconfig.json and some of the babel and webpack configs.",
|
|
||||||
severity: 'warn',
|
|
||||||
from: {
|
|
||||||
orphan: true,
|
|
||||||
pathNot: [
|
|
||||||
'(^|/)\\.[^/]+\\.(js|cjs|mjs|ts|json)$', // dot files
|
|
||||||
'\\.d\\.ts$', // TypeScript declaration files
|
|
||||||
'(^|/)tsconfig\\.json$', // TypeScript config
|
|
||||||
'(^|/)(babel|webpack)\\.config\\.(js|cjs|mjs|ts|json)$' // other configs
|
|
||||||
]
|
|
||||||
},
|
|
||||||
to: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'no-deprecated-core',
|
|
||||||
comment:
|
|
||||||
'A module depends on a node core module that has been deprecated. Find an alternative - these are ' +
|
|
||||||
"bound to exist - node doesn't deprecate lightly.",
|
|
||||||
severity: 'warn',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
dependencyTypes: [
|
|
||||||
'core'
|
|
||||||
],
|
|
||||||
path: [
|
|
||||||
'^(v8\/tools\/codemap)$',
|
|
||||||
'^(v8\/tools\/consarray)$',
|
|
||||||
'^(v8\/tools\/csvparser)$',
|
|
||||||
'^(v8\/tools\/logreader)$',
|
|
||||||
'^(v8\/tools\/profile_view)$',
|
|
||||||
'^(v8\/tools\/profile)$',
|
|
||||||
'^(v8\/tools\/SourceMap)$',
|
|
||||||
'^(v8\/tools\/splaytree)$',
|
|
||||||
'^(v8\/tools\/tickprocessor-driver)$',
|
|
||||||
'^(v8\/tools\/tickprocessor)$',
|
|
||||||
'^(node-inspect\/lib\/_inspect)$',
|
|
||||||
'^(node-inspect\/lib\/internal\/inspect_client)$',
|
|
||||||
'^(node-inspect\/lib\/internal\/inspect_repl)$',
|
|
||||||
'^(async_hooks)$',
|
|
||||||
'^(punycode)$',
|
|
||||||
'^(domain)$',
|
|
||||||
'^(constants)$',
|
|
||||||
'^(sys)$',
|
|
||||||
'^(_linklist)$',
|
|
||||||
'^(_stream_wrap)$'
|
|
||||||
],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'not-to-deprecated',
|
|
||||||
comment:
|
|
||||||
'This module uses a (version of an) npm module that has been deprecated. Either upgrade to a later ' +
|
|
||||||
'version of that module, or find an alternative. Deprecated modules are a security risk.',
|
|
||||||
severity: 'warn',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
dependencyTypes: [
|
|
||||||
'deprecated'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'no-non-package-json',
|
|
||||||
severity: 'error',
|
|
||||||
comment:
|
|
||||||
"This module depends on an npm package that isn't in the 'dependencies' section of your package.json. " +
|
|
||||||
"That's problematic as the package either (1) won't be available on live (2 - worse) will be " +
|
|
||||||
"available on live with an non-guaranteed version. Fix it by adding the package to the dependencies " +
|
|
||||||
"in your package.json.",
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
dependencyTypes: [
|
|
||||||
'npm-no-pkg',
|
|
||||||
'npm-unknown'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'not-to-unresolvable',
|
|
||||||
comment:
|
|
||||||
"This module depends on a module that cannot be found ('resolved to disk'). If it's an npm " +
|
|
||||||
'module: add it to your package.json. In all other cases you likely already know what to do.',
|
|
||||||
severity: 'error',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
couldNotResolve: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'no-duplicate-dep-types',
|
|
||||||
comment:
|
|
||||||
"Likely this module depends on an external ('npm') package that occurs more than once " +
|
|
||||||
"in your package.json i.e. bot as a devDependencies and in dependencies. This will cause " +
|
|
||||||
"maintenance problems later on.",
|
|
||||||
severity: 'warn',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
moreThanOneDependencyType: true,
|
|
||||||
// as it's pretty common to have a type import be a type only import
|
|
||||||
// _and_ (e.g.) a devDependency - don't consider type-only dependency
|
|
||||||
// types for this rule
|
|
||||||
dependencyTypesNot: ["type-only"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/* rules you might want to tweak for your specific situation: */
|
|
||||||
{
|
|
||||||
name: 'not-to-test',
|
|
||||||
comment:
|
|
||||||
"This module depends on code within a folder that should only contain tests. As tests don't " +
|
|
||||||
"implement functionality this is odd. Either you're writing a test outside the test folder " +
|
|
||||||
"or there's something in the test folder that isn't a test.",
|
|
||||||
severity: 'error',
|
|
||||||
from: {
|
|
||||||
pathNot: '^(tests)'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
path: '^(tests)'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'not-to-spec',
|
|
||||||
comment:
|
|
||||||
'This module depends on a spec (test) file. The sole responsibility of a spec file is to test code. ' +
|
|
||||||
"If there's something in a spec that's of use to other modules, it doesn't have that single " +
|
|
||||||
'responsibility anymore. Factor it out into (e.g.) a separate utility/ helper or a mock.',
|
|
||||||
severity: 'error',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
path: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'not-to-dev-dep',
|
|
||||||
severity: 'error',
|
|
||||||
comment:
|
|
||||||
"This module depends on an npm package from the 'devDependencies' section of your " +
|
|
||||||
'package.json. It looks like something that ships to production, though. To prevent problems ' +
|
|
||||||
"with npm packages that aren't there on production declare it (only!) in the 'dependencies'" +
|
|
||||||
'section of your package.json. If this module is development only - add it to the ' +
|
|
||||||
'from.pathNot re of the not-to-dev-dep rule in the dependency-cruiser configuration',
|
|
||||||
from: {
|
|
||||||
path: '^(src)',
|
|
||||||
pathNot: '\\.(spec|test)\\.(js|mjs|cjs|ts|ls|coffee|litcoffee|coffee\\.md)$'
|
|
||||||
},
|
|
||||||
to: {
|
|
||||||
dependencyTypes: [
|
|
||||||
'npm-dev'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'optional-deps-used',
|
|
||||||
severity: 'info',
|
|
||||||
comment:
|
|
||||||
"This module depends on an npm package that is declared as an optional dependency " +
|
|
||||||
"in your package.json. As this makes sense in limited situations only, it's flagged here. " +
|
|
||||||
"If you're using an optional dependency here by design - add an exception to your" +
|
|
||||||
"dependency-cruiser configuration.",
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
dependencyTypes: [
|
|
||||||
'npm-optional'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'peer-deps-used',
|
|
||||||
comment:
|
|
||||||
"This module depends on an npm package that is declared as a peer dependency " +
|
|
||||||
"in your package.json. This makes sense if your package is e.g. a plugin, but in " +
|
|
||||||
"other cases - maybe not so much. If the use of a peer dependency is intentional " +
|
|
||||||
"add an exception to your dependency-cruiser configuration.",
|
|
||||||
severity: 'warn',
|
|
||||||
from: {},
|
|
||||||
to: {
|
|
||||||
dependencyTypes: [
|
|
||||||
'npm-peer'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
options: {
|
|
||||||
|
|
||||||
/* conditions specifying which files not to follow further when encountered:
|
|
||||||
- path: a regular expression to match
|
|
||||||
- dependencyTypes: see https://github.com/sverweij/dependency-cruiser/blob/master/doc/rules-reference.md#dependencytypes-and-dependencytypesnot
|
|
||||||
for a complete list
|
|
||||||
*/
|
|
||||||
doNotFollow: {
|
|
||||||
path: 'node_modules'
|
|
||||||
},
|
|
||||||
|
|
||||||
/* conditions specifying which dependencies to exclude
|
|
||||||
- path: a regular expression to match
|
|
||||||
- dynamic: a boolean indicating whether to ignore dynamic (true) or static (false) dependencies.
|
|
||||||
leave out if you want to exclude neither (recommended!)
|
|
||||||
*/
|
|
||||||
// exclude : {
|
|
||||||
// path: '',
|
|
||||||
// dynamic: true
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* pattern specifying which files to include (regular expression)
|
|
||||||
dependency-cruiser will skip everything not matching this pattern
|
|
||||||
*/
|
|
||||||
// includeOnly : '',
|
|
||||||
|
|
||||||
/* dependency-cruiser will include modules matching against the focus
|
|
||||||
regular expression in its output, as well as their neighbours (direct
|
|
||||||
dependencies and dependents)
|
|
||||||
*/
|
|
||||||
// focus : '',
|
|
||||||
|
|
||||||
/* list of module systems to cruise */
|
|
||||||
// moduleSystems: ['amd', 'cjs', 'es6', 'tsd'],
|
|
||||||
|
|
||||||
/* prefix for links in html and svg output (e.g. 'https://github.com/you/yourrepo/blob/develop/'
|
|
||||||
to open it on your online repo or `vscode://file/${process.cwd()}/` to
|
|
||||||
open it in visual studio code),
|
|
||||||
*/
|
|
||||||
// prefix: '',
|
|
||||||
|
|
||||||
/* false (the default): ignore dependencies that only exist before typescript-to-javascript compilation
|
|
||||||
true: also detect dependencies that only exist before typescript-to-javascript compilation
|
|
||||||
"specify": for each dependency identify whether it only exists before compilation or also after
|
|
||||||
*/
|
|
||||||
tsPreCompilationDeps: true,
|
|
||||||
|
|
||||||
/*
|
|
||||||
list of extensions to scan that aren't javascript or compile-to-javascript.
|
|
||||||
Empty by default. Only put extensions in here that you want to take into
|
|
||||||
account that are _not_ parsable.
|
|
||||||
*/
|
|
||||||
// extraExtensionsToScan: [".json", ".jpg", ".png", ".svg", ".webp"],
|
|
||||||
|
|
||||||
/* if true combines the package.jsons found from the module up to the base
|
|
||||||
folder the cruise is initiated from. Useful for how (some) mono-repos
|
|
||||||
manage dependencies & dependency definitions.
|
|
||||||
*/
|
|
||||||
// combinedDependencies: false,
|
|
||||||
|
|
||||||
/* if true leave symlinks untouched, otherwise use the realpath */
|
|
||||||
// preserveSymlinks: false,
|
|
||||||
|
|
||||||
/* TypeScript project file ('tsconfig.json') to use for
|
|
||||||
(1) compilation and
|
|
||||||
(2) resolution (e.g. with the paths property)
|
|
||||||
|
|
||||||
The (optional) fileName attribute specifies which file to take (relative to
|
|
||||||
dependency-cruiser's current working directory). When not provided
|
|
||||||
defaults to './tsconfig.json'.
|
|
||||||
*/
|
|
||||||
tsConfig: {
|
|
||||||
fileName: 'tsconfig.json'
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Webpack configuration to use to get resolve options from.
|
|
||||||
|
|
||||||
The (optional) fileName attribute specifies which file to take (relative
|
|
||||||
to dependency-cruiser's current working directory. When not provided defaults
|
|
||||||
to './webpack.conf.js'.
|
|
||||||
|
|
||||||
The (optional) `env` and `args` attributes contain the parameters to be passed if
|
|
||||||
your webpack config is a function and takes them (see webpack documentation
|
|
||||||
for details)
|
|
||||||
*/
|
|
||||||
// webpackConfig: {
|
|
||||||
// fileName: './webpack.config.js',
|
|
||||||
// env: {},
|
|
||||||
// args: {},
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Babel config ('.babelrc', '.babelrc.json', '.babelrc.json5', ...) to use
|
|
||||||
for compilation (and whatever other naughty things babel plugins do to
|
|
||||||
source code). This feature is well tested and usable, but might change
|
|
||||||
behavior a bit over time (e.g. more precise results for used module
|
|
||||||
systems) without dependency-cruiser getting a major version bump.
|
|
||||||
*/
|
|
||||||
// babelConfig: {
|
|
||||||
// fileName: './.babelrc'
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* List of strings you have in use in addition to cjs/ es6 requires
|
|
||||||
& imports to declare module dependencies. Use this e.g. if you've
|
|
||||||
re-declared require, use a require-wrapper or use window.require as
|
|
||||||
a hack.
|
|
||||||
*/
|
|
||||||
// exoticRequireStrings: [],
|
|
||||||
/* options to pass on to enhanced-resolve, the package dependency-cruiser
|
|
||||||
uses to resolve module references to disk. You can set most of these
|
|
||||||
options in a webpack.conf.js - this section is here for those
|
|
||||||
projects that don't have a separate webpack config file.
|
|
||||||
|
|
||||||
Note: settings in webpack.conf.js override the ones specified here.
|
|
||||||
*/
|
|
||||||
enhancedResolveOptions: {
|
|
||||||
/* List of strings to consider as 'exports' fields in package.json. Use
|
|
||||||
['exports'] when you use packages that use such a field and your environment
|
|
||||||
supports it (e.g. node ^12.19 || >=14.7 or recent versions of webpack).
|
|
||||||
|
|
||||||
If you have an `exportsFields` attribute in your webpack config, that one
|
|
||||||
will have precedence over the one specified here.
|
|
||||||
*/
|
|
||||||
exportsFields: ["exports"],
|
|
||||||
/* List of conditions to check for in the exports field. e.g. use ['imports']
|
|
||||||
if you're only interested in exposed es6 modules, ['require'] for commonjs,
|
|
||||||
or all conditions at once `(['import', 'require', 'node', 'default']`)
|
|
||||||
if anything goes for you. Only works when the 'exportsFields' array is
|
|
||||||
non-empty.
|
|
||||||
|
|
||||||
If you have a 'conditionNames' attribute in your webpack config, that one will
|
|
||||||
have precedence over the one specified here.
|
|
||||||
*/
|
|
||||||
conditionNames: ["import", "require", "node", "default"],
|
|
||||||
/*
|
|
||||||
The extensions, by default are the same as the ones dependency-cruiser
|
|
||||||
can access (run `npx depcruise --info` to see which ones that are in
|
|
||||||
_your_ environment. If that list is larger than what you need (e.g.
|
|
||||||
it contains .js, .jsx, .ts, .tsx, .cts, .mts - but you don't use
|
|
||||||
TypeScript you can pass just the extensions you actually use (e.g.
|
|
||||||
[".js", ".jsx"]). This can speed up the most expensive step in
|
|
||||||
dependency cruising (module resolution) quite a bit.
|
|
||||||
*/
|
|
||||||
// extensions: [".js", ".jsx", ".ts", ".tsx", ".d.ts"],
|
|
||||||
/*
|
|
||||||
If your TypeScript project makes use of types specified in 'types'
|
|
||||||
fields in package.jsons of external dependencies, specify "types"
|
|
||||||
in addition to "main" in here, so enhanced-resolve (the resolver
|
|
||||||
dependency-cruiser uses) knows to also look there. You can also do
|
|
||||||
this if you're not sure, but still use TypeScript. In a future version
|
|
||||||
of dependency-cruiser this will likely become the default.
|
|
||||||
*/
|
|
||||||
mainFields: ["main", "types"],
|
|
||||||
},
|
|
||||||
reporterOptions: {
|
|
||||||
dot: {
|
|
||||||
/* pattern of modules that can be consolidated in the detailed
|
|
||||||
graphical dependency graph. The default pattern in this configuration
|
|
||||||
collapses everything in node_modules to one folder deep so you see
|
|
||||||
the external modules, but not the innards your app depends upon.
|
|
||||||
*/
|
|
||||||
collapsePattern: 'node_modules/(@[^/]+/[^/]+|[^/]+)',
|
|
||||||
|
|
||||||
/* Options to tweak the appearance of your graph.See
|
|
||||||
https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions
|
|
||||||
for details and some examples. If you don't specify a theme
|
|
||||||
don't worry - dependency-cruiser will fall back to the default one.
|
|
||||||
*/
|
|
||||||
// theme: {
|
|
||||||
// graph: {
|
|
||||||
// /* use splines: "ortho" for straight lines. Be aware though
|
|
||||||
// graphviz might take a long time calculating ortho(gonal)
|
|
||||||
// routings.
|
|
||||||
// */
|
|
||||||
// splines: "true"
|
|
||||||
// },
|
|
||||||
// modules: [
|
|
||||||
// {
|
|
||||||
// criteria: { matchesFocus: true },
|
|
||||||
// attributes: {
|
|
||||||
// fillcolor: "lime",
|
|
||||||
// penwidth: 2,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { matchesFocus: false },
|
|
||||||
// attributes: {
|
|
||||||
// fillcolor: "lightgrey",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { matchesReaches: true },
|
|
||||||
// attributes: {
|
|
||||||
// fillcolor: "lime",
|
|
||||||
// penwidth: 2,
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { matchesReaches: false },
|
|
||||||
// attributes: {
|
|
||||||
// fillcolor: "lightgrey",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { source: "^src/model" },
|
|
||||||
// attributes: { fillcolor: "#ccccff" }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { source: "^src/view" },
|
|
||||||
// attributes: { fillcolor: "#ccffcc" }
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// dependencies: [
|
|
||||||
// {
|
|
||||||
// criteria: { "rules[0].severity": "error" },
|
|
||||||
// attributes: { fontcolor: "red", color: "red" }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { "rules[0].severity": "warn" },
|
|
||||||
// attributes: { fontcolor: "orange", color: "orange" }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { "rules[0].severity": "info" },
|
|
||||||
// attributes: { fontcolor: "blue", color: "blue" }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { resolved: "^src/model" },
|
|
||||||
// attributes: { color: "#0000ff77" }
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// criteria: { resolved: "^src/view" },
|
|
||||||
// attributes: { color: "#00770077" }
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
archi: {
|
|
||||||
/* pattern of modules that can be consolidated in the high level
|
|
||||||
graphical dependency graph. If you use the high level graphical
|
|
||||||
dependency graph reporter (`archi`) you probably want to tweak
|
|
||||||
this collapsePattern to your situation.
|
|
||||||
*/
|
|
||||||
collapsePattern: '^(packages|src|lib|app|bin|test(s?)|spec(s?))/[^/]+|node_modules/(@[^/]+/[^/]+|[^/]+)',
|
|
||||||
|
|
||||||
/* Options to tweak the appearance of your graph.See
|
|
||||||
https://github.com/sverweij/dependency-cruiser/blob/master/doc/options-reference.md#reporteroptions
|
|
||||||
for details and some examples. If you don't specify a theme
|
|
||||||
for 'archi' dependency-cruiser will use the one specified in the
|
|
||||||
dot section (see above), if any, and otherwise use the default one.
|
|
||||||
*/
|
|
||||||
// theme: {
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
"text": {
|
|
||||||
"highlightFocused": true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// generated: dependency-cruiser@12.11.0 on 2023-03-24T14:11:38.647Z
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
VITE_APP_API=/api
|
|
||||||
#登录与权限关闭
|
|
||||||
VITE_APP_PM_ENABLED=false
|
|
||||||
VITE_APP_TITLE=fs-admin-antdv4
|
|
||||||
VITE_APP_SLOGAN=面向配置的CRUD开发,快如闪电
|
|
||||||
VITE_APP_COPYRIGHT=Copyright © 2021 Greper
|
|
||||||
VITE_APP_LOGO_PATH=./images/logo/logo.svg
|
|
||||||
VITE_APP_PROJECT_PATH=https://github.com/fast-crud/fast-crud
|
|
||||||
VITE_APP_NAMESPACE=fs
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#登录与权限开启
|
|
||||||
VITE_APP_PM_ENABLED=false
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#登录与权限开启
|
|
||||||
VITE_APP_PM_ENABLED=true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#登录与权限开启
|
|
||||||
VITE_APP_PM_ENABLED=true
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
VITE_APP_API=http://www.docmirror.cn:7001/api
|
|
||||||
#登录与权限开启
|
|
||||||
VITE_APP_PM_ENABLED=true
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.idea
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: {
|
|
||||||
browser: true,
|
|
||||||
node: true,
|
|
||||||
es6: true
|
|
||||||
},
|
|
||||||
parser: "vue-eslint-parser",
|
|
||||||
parserOptions: {
|
|
||||||
parser: "@typescript-eslint/parser",
|
|
||||||
ecmaVersion: 2020,
|
|
||||||
sourceType: "module",
|
|
||||||
jsxPragma: "React",
|
|
||||||
ecmaFeatures: {
|
|
||||||
jsx: true,
|
|
||||||
tsx: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extends: ["plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "plugin:prettier/recommended", "prettier"],
|
|
||||||
rules: {
|
|
||||||
//"max-len": [0, 200, 2, { ignoreUrls: true }],
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/ban-ts-ignore": "off",
|
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
|
||||||
"@typescript-eslint/no-var-requires": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
|
||||||
"@typescript-eslint/ban-types": "off",
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off",
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
|
||||||
// "@typescript-eslint/no-unused-vars": [
|
|
||||||
// "error",
|
|
||||||
// {
|
|
||||||
// argsIgnorePattern: "^h$",
|
|
||||||
// varsIgnorePattern: "^h$",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// "no-unused-vars": [
|
|
||||||
// "error",
|
|
||||||
// {
|
|
||||||
// argsIgnorePattern: "^h$",
|
|
||||||
// varsIgnorePattern: "^h$",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
// "vue/custom-event-name-casing": "off",
|
|
||||||
// "no-use-before-define": "off",
|
|
||||||
// "space-before-function-paren": "off",
|
|
||||||
|
|
||||||
// "vue/attributes-order": "off",
|
|
||||||
// "vue/one-component-per-file": "off",
|
|
||||||
// "vue/html-closing-bracket-newline": "off",
|
|
||||||
// "vue/max-attributes-per-line": "off",
|
|
||||||
// "vue/multiline-html-element-content-newline": "off",
|
|
||||||
// "vue/singleline-html-element-content-newline": "off",
|
|
||||||
// "vue/attribute-hyphenation": "off",
|
|
||||||
// "vue/require-default-prop": "off",
|
|
||||||
// "vue/html-self-closing": [
|
|
||||||
// "error",
|
|
||||||
// {
|
|
||||||
// html: {
|
|
||||||
// void: "always",
|
|
||||||
// normal: "never",
|
|
||||||
// component: "always",
|
|
||||||
// },
|
|
||||||
// svg: "always",
|
|
||||||
// math: "always",
|
|
||||||
// },
|
|
||||||
// ],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
> 感谢您支持fast-crud,请按如下规范提交issue
|
|
||||||
> 如果有条件,请尽量在[github上提交](https://github.com/fast-crud/fast-crud/issues)
|
|
||||||
|
|
||||||
|
|
||||||
## 一、问题描述
|
|
||||||
`请在此处简要描述你所遇到的问题,必要时请贴出相关截图辅助理解和定位`
|
|
||||||
|
|
||||||
### 复现步骤
|
|
||||||
`请描述复现问题的详细步骤`
|
|
||||||
`如果非示例页面的问题,最好能提供最小复现示例的代码、或者仓库链接`
|
|
||||||
|
|
||||||
### 代码截图
|
|
||||||
`请贴出出错的相关代码截图`
|
|
||||||
|
|
||||||
### 报错截图
|
|
||||||
`请贴出报错日志截图`
|
|
||||||
|
|
||||||
### 效果截图
|
|
||||||
`请贴出效果截图`
|
|
||||||
#### 1. 期望效果
|
|
||||||
|
|
||||||
#### 2. 实际效果
|
|
||||||
|
|
||||||
|
|
||||||
## 二、当前使用的库版本
|
|
||||||
### 1. fast-crud版本:
|
|
||||||
`请您填写fast-crud的版本`
|
|
||||||
### 2. 使用的ui库以及版本
|
|
||||||
`中括号中输入x即选中,或者删除其他,仅保留你正使用的ui库`
|
|
||||||
- [x] Antdv (版本?)
|
|
||||||
- [ ] ElementPlus(版本?)
|
|
||||||
- [ ] NaiveUI(版本?)
|
|
||||||
|
|
||||||
### 3. 使用的admin框架
|
|
||||||
`请您填写您当前使用的admin框架是哪一套`
|
|
||||||
- [x] fs-admin-antdv
|
|
||||||
- [ ] fs-admin-element
|
|
||||||
- [ ] fs-admin-naive
|
|
||||||
- [ ] fs-in-vben
|
|
||||||
- [ ] vben-admin
|
|
||||||
- [ ] cool-admin
|
|
||||||
- [ ] 其他:`请注明`
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
name: sync-to-gitee
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ "main" ]
|
|
||||||
# schedule:
|
|
||||||
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
|
|
||||||
# - cron: '17 19 * * *'
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout work repo # 1. 检出当前仓库(certd-sync-work)
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
|
|
||||||
run: |
|
|
||||||
git config --global user.name "xiaojunnuo"
|
|
||||||
git config --global user.email "xiaojunnuo@qq.com"
|
|
||||||
|
|
||||||
- name: Set git token # 3. 给git命令设置token,用于push到目标仓库
|
|
||||||
uses: de-vri-es/setup-git-credentials@v2
|
|
||||||
with:
|
|
||||||
credentials: https://${{secrets.PUSH_TOKEN_GITEE}}@gitee.com
|
|
||||||
|
|
||||||
- name: push to gitee # 4. 执行同步
|
|
||||||
run: |
|
|
||||||
git remote add upstream https://gitee.com/fast-crud/fs-admin-antdv4
|
|
||||||
git push --set-upstream upstream main
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
11
packages/ui/certd-client/.gitignore
vendored
11
packages/ui/certd-client/.gitignore
vendored
@@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
*.local
|
|
||||||
/stats.html
|
|
||||||
yarn.lock
|
|
||||||
.idea
|
|
||||||
/.idea/
|
|
||||||
yarn-error.log
|
|
||||||
vite-profile.cpuprofile
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
/stats.html
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
link-workspace-packages=deep
|
|
||||||
prefer-workspace-packages=true
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
|
|
||||||
"trailingComma": "none",
|
|
||||||
"printWidth": 220
|
|
||||||
}
|
|
||||||
@@ -1,689 +0,0 @@
|
|||||||
# Change Log
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
||||||
|
|
||||||
# [1.26.0](https://github.com/fast-crud/fast-crud/compare/v1.25.13...v1.26.0) (2025-07-28)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 独立使用form表单缺失mode的问题 ([9ed791a](https://github.com/fast-crud/fast-crud/commit/9ed791ad4bb9294f4b9380d858df89fbc32ca2a0))
|
|
||||||
* 修复table-select 示例右上角自定义插槽无法设置的bug ([54a5d90](https://github.com/fast-crud/fast-crud/commit/54a5d90b86338036474657267de3bd7a74caf1eb))
|
|
||||||
* card布局情况下,header-top header-bottom同时跟search显隐的bug ([3484232](https://github.com/fast-crud/fast-crud/commit/348423280f06f052fb16214e863ba03735ff9042))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* antdv示例背景设置为白色 ([3d74bf4](https://github.com/fast-crud/fast-crud/commit/3d74bf4e7ca76ecad286ec8f1b8fd2cbcb6428eb))
|
|
||||||
|
|
||||||
## [1.25.13](https://github.com/fast-crud/fast-crud/compare/v1.25.12...v1.25.13) (2025-06-10)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复fs-values-format组件某些值无法获取自动颜色的bug ([18169fc](https://github.com/fast-crud/fast-crud/commit/18169fc11f37595b8fba96467f0c741fe898ad21))
|
|
||||||
|
|
||||||
## [1.25.12](https://github.com/fast-crud/fast-crud/compare/v1.25.11...v1.25.12) (2025-05-26)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复cloneable模式下的dict 无法动态修改data的bug ([1dd5cd1](https://github.com/fast-crud/fast-crud/commit/1dd5cd1e1445b126451c6e1b68f453a70bf920de))
|
|
||||||
|
|
||||||
## [1.25.11](https://github.com/fast-crud/fast-crud/compare/v1.25.10...v1.25.11) (2025-05-14)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复naive-ui下 form-item 的label设置为render会报警告的问题 ([45e1bc6](https://github.com/fast-crud/fast-crud/commit/45e1bc6d9cfc408a98dfe628bf3b9bd14af4b2cf))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 单元格支持tooltip ([44d83ad](https://github.com/fast-crud/fast-crud/commit/44d83ad890589b2b64ff5e9f869fc04863576a3b))
|
|
||||||
|
|
||||||
## [1.25.10](https://github.com/fast-crud/fast-crud/compare/v1.25.9...v1.25.10) (2025-04-25)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 新增editable-select组件 ([8681285](https://github.com/fast-crud/fast-crud/commit/86812851de435cb2406d06898396c368d5eda414))
|
|
||||||
* 优化antdv单元格合并示例,使用customCell方法,以及增加操作列合并演示 ([1068f9a](https://github.com/fast-crud/fast-crud/commit/1068f9aaa9b7732acb7082cc2ce3b1fadf1f8521))
|
|
||||||
|
|
||||||
## [1.25.9](https://github.com/fast-crud/fast-crud/compare/v1.25.8...v1.25.9) (2025-04-16)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复预览大图previewurl错误的bug ([cfb6554](https://github.com/fast-crud/fast-crud/commit/cfb6554c7c93297ddfcaa206168aceaf4ba2c2ef))
|
|
||||||
* 修复yaml workers引入问题 ([1c90198](https://github.com/fast-crud/fast-crud/commit/1c90198f6f7df0ac0c9845d1c6af0592b1c34ae3))
|
|
||||||
|
|
||||||
## [1.25.8](https://github.com/fast-crud/fast-crud/compare/v1.25.7...v1.25.8) (2025-04-10)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复commonOptions无法覆盖某些参数的bug ([f2ecc03](https://github.com/fast-crud/fast-crud/commit/f2ecc034bf5b38d668ee8366c903a824af34302c))
|
|
||||||
* fs-editor-code 支持配置schema校验 ([7d342cb](https://github.com/fast-crud/fast-crud/commit/7d342cbe8ebbaebb6ff5b3b80ce977d87aaa9ba5))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 添加代码编辑器示例 ([7217460](https://github.com/fast-crud/fast-crud/commit/72174604735b90fc57e0fb1ce40cc380b6c0c351))
|
|
||||||
|
|
||||||
## [1.25.7](https://github.com/fast-crud/fast-crud/compare/v1.25.6...v1.25.7) (2025-03-30)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复新页面编辑无法正确获取数据的bug ([e0df772](https://github.com/fast-crud/fast-crud/commit/e0df7729d0d8fff7a0bcd81477ec9379f6f23369))
|
|
||||||
* 修复antdv4示例没有源码跳转按钮的bug ([a8f6486](https://github.com/fast-crud/fast-crud/commit/a8f6486bccc441bb394ae5fb8bbe515de78f83d3))
|
|
||||||
|
|
||||||
## [1.25.6](https://github.com/fast-crud/fast-crud/compare/v1.25.5...v1.25.6) (2025-03-19)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.25.5](https://github.com/fast-crud/fast-crud/compare/v1.25.4...v1.25.5) (2025-03-19)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复 antdv 弹出菜单边框过大的问题 ([fe4a044](https://github.com/fast-crud/fast-crud/commit/fe4a0442bf8fcdc3120b6de788ff318933b6bfab))
|
|
||||||
* 修复 antdv懒加载后dropdown按钮无法点击的bug ([30ee067](https://github.com/fast-crud/fast-crud/commit/30ee067580fb663bbe550d50abf63c1fd89504a1))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* antdv示例增加保存列宽功能 ([a1218b0](https://github.com/fast-crud/fast-crud/commit/a1218b0451eb73fae8e337128e79b6e1fd4184eb))
|
|
||||||
|
|
||||||
## [1.25.4](https://github.com/fast-crud/fast-crud/compare/v1.25.3...v1.25.4) (2025-03-04)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 精简lodash ([21f59e8](https://github.com/fast-crud/fast-crud/commit/21f59e80db56cf0968b2739b9bc7ff1e8b7be4e4))
|
|
||||||
* antdv 异步加载,加快首页打开速度 ([4eb4283](https://github.com/fast-crud/fast-crud/commit/4eb4283ad66e856814962ca2bde416dddbac0868))
|
|
||||||
|
|
||||||
## [1.25.3](https://github.com/fast-crud/fast-crud/compare/v1.25.2...v1.25.3) (2025-02-23)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.25.2](https://github.com/fast-crud/fast-crud/compare/v1.25.1...v1.25.2) (2025-02-22)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复4.2.x版本antdv导致modal全屏无效的bug ([9a26363](https://github.com/fast-crud/fast-crud/commit/9a26363d44faf6b0eb0809be4a8dd0fd14f2c309))
|
|
||||||
|
|
||||||
## [1.25.1](https://github.com/fast-crud/fast-crud/compare/v1.25.0...v1.25.1) (2025-02-12)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
# [1.25.0](https://github.com/fast-crud/fast-crud/compare/v1.24.2...v1.25.0) (2025-01-12)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 支持图标选择器 ([7dd8745](https://github.com/fast-crud/fast-crud/commit/7dd874534caa926ba63d6b8100116c032d794d51))
|
|
||||||
|
|
||||||
## [1.24.2](https://github.com/fast-crud/fast-crud/compare/v1.24.1...v1.24.2) (2024-12-31)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.24.1](https://github.com/fast-crud/fast-crud/compare/v1.24.0...v1.24.1) (2024-12-28)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
# [1.24.0](https://github.com/fast-crud/fast-crud/compare/v1.23.4...v1.24.0) (2024-12-28)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.23.4](https://github.com/fast-crud/fast-crud/compare/v1.23.3...v1.23.4) (2024-12-03)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复表单全屏的bug ([a25cff7](https://github.com/fast-crud/fast-crud/commit/a25cff725bef9d0dac063f7bd653844231c57b8d))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* rowHandle按钮支持render,删除按钮提供popcomfirm风格示例 ([b834db9](https://github.com/fast-crud/fast-crud/commit/b834db96e46e6b281d5c3a178a57c71aadf9bfd0))
|
|
||||||
* table-select open支持context参数 ([492ee98](https://github.com/fast-crud/fast-crud/commit/492ee9862eee80ffcef81f42178a33484102213a))
|
|
||||||
|
|
||||||
## [1.23.3](https://github.com/fast-crud/fast-crud/compare/v1.23.2...v1.23.3) (2024-11-26)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复antdv4新页面打开示例不显示表单的bug ([34ab106](https://github.com/fast-crud/fast-crud/commit/34ab106d5e1cce918ce9745df141fda03f69331d))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 增加card列表示例 ([cfad8c8](https://github.com/fast-crud/fast-crud/commit/cfad8c87cb6f9588bb016c0595d03b34b0147c2a))
|
|
||||||
|
|
||||||
## [1.23.2](https://github.com/fast-crud/fast-crud/compare/v1.23.1...v1.23.2) (2024-11-18)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复dict-select多选情况下selected-change返回为空的bug ([181b167](https://github.com/fast-crud/fast-crud/commit/181b167d29536ffd8cd476e4744e322c5542b991))
|
|
||||||
|
|
||||||
## [1.23.1](https://github.com/fast-crud/fast-crud/compare/v1.23.0...v1.23.1) (2024-11-13)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复1.23.0 antdv下不显示pagination的bug ([9424dc1](https://github.com/fast-crud/fast-crud/commit/9424dc130505a5557042534acb976400b42ff483))
|
|
||||||
|
|
||||||
# [1.23.0](https://github.com/fast-crud/fast-crud/compare/v1.22.5...v1.23.0) (2024-11-11)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 示例全面改成useFsAsync ([aa848a9](https://github.com/fast-crud/fast-crud/commit/aa848a9530af831247077620fa3ee0f605c19009))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 示例改成useFsAsync ([f7fac52](https://github.com/fast-crud/fast-crud/commit/f7fac52fcfaa4703bfebd8259007b235401b8357))
|
|
||||||
|
|
||||||
## [1.22.5](https://github.com/fast-crud/fast-crud/compare/v1.22.4...v1.22.5) (2024-11-04)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.22.4](https://github.com/fast-crud/fast-crud/compare/v1.22.3...v1.22.4) (2024-11-04)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复tab change后清空查询表单的bug ([8d7525a](https://github.com/fast-crud/fast-crud/commit/8d7525a747c41fe3fb1f14b92d0e353440ebc8ad))
|
|
||||||
|
|
||||||
## [1.22.3](https://github.com/fast-crud/fast-crud/compare/v1.22.2...v1.22.3) (2024-11-01)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复search.formItem配置无效的bug ([e112f03](https://github.com/fast-crud/fast-crud/commit/e112f033a60e142eced129e03b7c3cb96605b3fb))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* editable row 优化添加 ([9e0beb0](https://github.com/fast-crud/fast-crud/commit/9e0beb06470b564d490ec9393afe978f8a66c2fd))
|
|
||||||
|
|
||||||
## [1.22.2](https://github.com/fast-crud/fast-crud/compare/v1.22.1...v1.22.2) (2024-10-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.22.1](https://github.com/fast-crud/fast-crud/compare/v1.22.0...v1.22.1) (2024-10-23)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 表单支持左右插槽 ([e3a9e8b](https://github.com/fast-crud/fast-crud/commit/e3a9e8b985558f7cbff0acd580af242df56da8c4))
|
|
||||||
* 独立使用表单支持插槽 ([095da7a](https://github.com/fast-crud/fast-crud/commit/095da7ac92996779f3b3c3885a8abd4de6c2fc0c))
|
|
||||||
* editable支持单元格插槽 ([ae029de](https://github.com/fast-crud/fast-crud/commit/ae029de0f554f4cc4c4750e0af3b6f7bd1edaee5))
|
|
||||||
* values-format option支持iconSpin ([cdaa4f5](https://github.com/fast-crud/fast-crud/commit/cdaa4f55a9384b95764443b4a7f4223ec787cce3))
|
|
||||||
|
|
||||||
# [1.22.0](https://github.com/fast-crud/fast-crud/compare/v1.21.5...v1.22.0) (2024-10-21)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.21.5](https://github.com/fast-crud/fast-crud/compare/v1.21.4...v1.21.5) (2024-10-21)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 列设置支持自定义storage ([298fb2f](https://github.com/fast-crud/fast-crud/commit/298fb2f9f2fff559567bedf6e977e7cb04024cdd))
|
|
||||||
|
|
||||||
## [1.21.4](https://github.com/fast-crud/fast-crud/compare/v1.21.3...v1.21.4) (2024-10-13)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 优化列设置多级表头支持级联勾选 ([a196922](https://github.com/fast-crud/fast-crud/commit/a196922630e9ef627dd548bb8d1c13acbe2eee28))
|
|
||||||
* table-select支持destroyOnClose参数,以修复点击取消后,扔保留上一次选中值的bug ([5a70cec](https://github.com/fast-crud/fast-crud/commit/5a70cec7e5f45439a518fc2aadc40b29fad5c6f1))
|
|
||||||
|
|
||||||
## [1.21.3](https://github.com/fast-crud/fast-crud/compare/v1.21.2...v1.21.3) (2024-09-20)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 优化antdv search按钮组错位问题 ([0948650](https://github.com/fast-crud/fast-crud/commit/0948650747d725ffe84e4f357d81a3a9a331109e))
|
|
||||||
|
|
||||||
## [1.21.2](https://github.com/fast-crud/fast-crud/compare/v1.21.1...v1.21.2) (2024-07-15)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 增加示例,FsInDrawer ([777830f](https://github.com/fast-crud/fast-crud/commit/777830f860b6a9752ba24df5a99af5e7c62bdfb2))
|
|
||||||
|
|
||||||
## [1.21.1](https://github.com/fast-crud/fast-crud/compare/v1.21.0...v1.21.1) (2024-06-23)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复独立使用对话框 openDialog方法await无返回值的bug ([0cc22fd](https://github.com/fast-crud/fast-crud/commit/0cc22fd2ad57b8e3e85174ced1546bb6a90ed838))
|
|
||||||
|
|
||||||
# [1.21.0](https://github.com/fast-crud/fast-crud/compare/v1.20.2...v1.21.0) (2024-06-08)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复三级以上路由页面无法缓存的问题 ([9ce8c7a](https://github.com/fast-crud/fast-crud/commit/9ce8c7a6a5fc12f347351ca74213ff9c542820d3))
|
|
||||||
* 修复提交表单只有一个输入框时点回车会刷新浏览器的问题 ([3d756ea](https://github.com/fast-crud/fast-crud/commit/3d756eaab6894355053e078424835d3edbd80025))
|
|
||||||
* 修复fs-table.less中的颜色污染 ([a0b1de4](https://github.com/fast-crud/fast-crud/commit/a0b1de45668f882e13669c5aee484d3d2532ce25))
|
|
||||||
* edit-wang 改成edit-wang5 ([7b994c1](https://github.com/fast-crud/fast-crud/commit/7b994c19637aa4b6399acbf362d4dc6a73c07ca4))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 富文本编辑器增加为空校验示例 ([3e51ac1](https://github.com/fast-crud/fast-crud/commit/3e51ac1c2c22f2dc5200134732fc0146a4a0fd2d))
|
|
||||||
* 图片裁剪组件中英文支持 ([8b5b3f6](https://github.com/fast-crud/fast-crud/commit/8b5b3f61bc67e17ed13ded4a5519434249d5c4df))
|
|
||||||
* alioss getAuthorization接口支持后台返回key ([75e5b14](https://github.com/fast-crud/fast-crud/commit/75e5b1449238fbae86f002c290771a6b9fd1f824))
|
|
||||||
|
|
||||||
## [1.20.2](https://github.com/fast-crud/fast-crud/compare/v1.20.1...v1.20.2) (2024-03-21)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复单元格valueChange 的value 改变滞后的问题 ([768c233](https://github.com/fast-crud/fast-crud/commit/768c233c915dc4277f3fb49106bf99f0bd1fb31c))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 升级依赖版本 ([42ae562](https://github.com/fast-crud/fast-crud/commit/42ae56289cc9d80ee1b3c1f9b7b2dd4656e9ba84))
|
|
||||||
* table-select element增加radio列 ([b56b5df](https://github.com/fast-crud/fast-crud/commit/b56b5df79c6ce634bdac0545e83629f6f5587d42))
|
|
||||||
|
|
||||||
## [1.20.1](https://github.com/fast-crud/fast-crud/compare/v1.20.0...v1.20.1) (2024-02-27)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
# [1.20.0](https://github.com/fast-crud/fast-crud/compare/v1.19.3...v1.20.0) (2024-01-28)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复查询valueChange 修改form无效的bug ([054f8b4](https://github.com/fast-crud/fast-crud/commit/054f8b4b808a52f6d8daf2d19ee3adf43f693c0a))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 优化element 日期示例,格式化问题,输入数据格式告警问题,range样式问题 ([66fd07b](https://github.com/fast-crud/fast-crud/commit/66fd07b96143b77ed73b5e3b88182070ebdb4c80))
|
|
||||||
* 优化form.wrapper.buttons的默认配置 ([61f2ae5](https://github.com/fast-crud/fast-crud/commit/61f2ae5600814a59e2eaad8933892c1ec9f57c69))
|
|
||||||
* 优化free模式,支持默认不激活 ([aeaf0a6](https://github.com/fast-crud/fast-crud/commit/aeaf0a683ecc24dcb86036daea363f3019347299))
|
|
||||||
* dict-tree组件无需手动配置labelName keyName ([c8b0ee1](https://github.com/fast-crud/fast-crud/commit/c8b0ee1ee5fa22e73b3a8ef77e1ad3335351dc70))
|
|
||||||
|
|
||||||
## [1.19.3](https://github.com/fast-crud/fast-crud/compare/v1.19.2...v1.19.3) (2023-12-15)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* antdv4 日期组件bug修复 ([a55b3e2](https://github.com/fast-crud/fast-crud/commit/a55b3e293a94396bbdfbd7d6dabb19d886cb8e16))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 增加表单label=0px示例 ([500d793](https://github.com/fast-crud/fast-crud/commit/500d793d72d727e8945cf7bca47aee684856bd80))
|
|
||||||
|
|
||||||
## [1.19.2](https://github.com/fast-crud/fast-crud/compare/v1.19.1...v1.19.2) (2023-11-22)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **editable:** 行编辑只能删除第一条数据的bug ([daf041f](https://github.com/fast-crud/fast-crud/commit/daf041f21cf531b4e32655248e522c96dd06f460))
|
|
||||||
|
|
||||||
## [1.19.1](https://github.com/fast-crud/fast-crud/compare/v1.19.0...v1.19.1) (2023-11-20)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复一些错误的类型定义 ([e098f51](https://github.com/fast-crud/fast-crud/commit/e098f511160148a824a1950bf4e85325c2ac50f0))
|
|
||||||
|
|
||||||
# [1.19.0](https://github.com/fast-crud/fast-crud/compare/v1.18.5...v1.19.0) (2023-11-20)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* **editable:** 支持多级数据 ([89db59e](https://github.com/fast-crud/fast-crud/commit/89db59ea2b3dbe8227399086513e27aa7c2ab7aa))
|
|
||||||
|
|
||||||
## [1.18.5](https://github.com/fast-crud/fast-crud/compare/v1.18.4...v1.18.5) (2023-11-08)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复form.value会覆盖初始值的bug ([050f889](https://github.com/fast-crud/fast-crud/commit/050f889dfbdfb38debcd7c8e4a455acf07198530))
|
|
||||||
|
|
||||||
## [1.18.4](https://github.com/fast-crud/fast-crud/compare/v1.18.3...v1.18.4) (2023-11-07)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复afterSubmit返回false仍然关闭对话框的bug ([80337ff](https://github.com/fast-crud/fast-crud/commit/80337ffc46eda74d526562d9f27c43a2b6eb0534))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 新增国际手机号输入框 ([ebabee2](https://github.com/fast-crud/fast-crud/commit/ebabee2f61caed3678f0681330ed3cb044803a2f))
|
|
||||||
* antdv 支持按钮组 ([cfdefdf](https://github.com/fast-crud/fast-crud/commit/cfdefdf89bfe7e037d1a8d3c6416cf38678074c9))
|
|
||||||
|
|
||||||
## [1.18.3](https://github.com/fast-crud/fast-crud/compare/v1.18.2...v1.18.3) (2023-10-26)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.18.2](https://github.com/fast-crud/fast-crud/compare/v1.18.1...v1.18.2) (2023-10-26)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 导出配置columns报错的bug ([d12f881](https://github.com/fast-crud/fast-crud/commit/d12f881f83e8c521673dc49d656e457a4fc67102))
|
|
||||||
* 修复动态切换component.name报 resolveComponent 只能在setup和render中使用的问题 ([8792962](https://github.com/fast-crud/fast-crud/commit/8792962156346dbf05445d8f143b23296d60c781))
|
|
||||||
|
|
||||||
## [1.18.1](https://github.com/fast-crud/fast-crud/compare/v1.18.0...v1.18.1) (2023-10-26)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 取消 searchCopyFormProps valueResolve配置 ([ae55fda](https://github.com/fast-crud/fast-crud/commit/ae55fda1f9aa206d644f2e3da654201f0831f0be))
|
|
||||||
|
|
||||||
# [1.18.0](https://github.com/fast-crud/fast-crud/compare/v1.17.5...v1.18.0) (2023-10-25)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复antdv4,drawer弹窗过时的api ([9514db6](https://github.com/fast-crud/fast-crud/commit/9514db6768b5a5e1bef283b961438a6671f7df79))
|
|
||||||
* 修复element下按钮图标异常问题 ([4959c2e](https://github.com/fast-crud/fast-crud/commit/4959c2e15b89f6d2fec50864f1453f2965a85159))
|
|
||||||
* 增加文档链接 ([2b9f525](https://github.com/fast-crud/fast-crud/commit/2b9f525988c34ea322695b1a40de0628a627e50a))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* 新特性,CrudOptionsPlugin ([9e1ac6d](https://github.com/fast-crud/fast-crud/commit/9e1ac6df56622b3b75cd5a23ea565f5c722085de))
|
|
||||||
* ui-demo,ui-interface独立 ([d78f040](https://github.com/fast-crud/fast-crud/commit/d78f040cd666d072937b0350edb2da11871206e6))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 导出增加loading ([6530c29](https://github.com/fast-crud/fast-crud/commit/6530c29615be9e1ff04029a962d521bed2df30a6))
|
|
||||||
* 优化文档搜索 ([19fff41](https://github.com/fast-crud/fast-crud/commit/19fff41b3f431e2bd1c84274a7d17ad96a547b03))
|
|
||||||
|
|
||||||
## [1.17.5](https://github.com/fast-crud/fast-crud/compare/v1.17.4...v1.17.5) (2023-09-26)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.17.4](https://github.com/fast-crud/fast-crud/compare/v1.17.3...v1.17.4) (2023-09-26)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.17.3](https://github.com/fast-crud/fast-crud/compare/v1.17.2...v1.17.3) (2023-09-23)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.17.2](https://github.com/fast-crud/fast-crud/compare/v1.17.1...v1.17.2) (2023-09-16)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复naive 时间示例无法修改的bug ([6ab9218](https://github.com/fast-crud/fast-crud/commit/6ab92188fc19d792de8bed0190853c444784b009))
|
|
||||||
* antdv 查询框label上置错位的bug ([00a35ad](https://github.com/fast-crud/fast-crud/commit/00a35ade86de3f2b9c3c336f3c8dda6f224e1abf))
|
|
||||||
|
|
||||||
## [1.17.1](https://github.com/fast-crud/fast-crud/compare/v1.17.0...v1.17.1) (2023-09-13)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
# [1.17.0](https://github.com/fast-crud/fast-crud/compare/v1.16.11...v1.17.0) (2023-09-12)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复element 图片裁剪组件无法横向排列的bug ([25fa258](https://github.com/fast-crud/fast-crud/commit/25fa25855e9750813d5a959a8b09ff6e90b04c1c))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* table-select支持 ([1c5b749](https://github.com/fast-crud/fast-crud/commit/1c5b7493a7782581a5f2a5bff843b135eb531f92))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 增加重置后清空排序设置演示 ([6a563ad](https://github.com/fast-crud/fast-crud/commit/6a563ad67b87f66e2765e47f72c5d4831cf06801))
|
|
||||||
|
|
||||||
## [1.16.11](https://github.com/fast-crud/fast-crud/compare/v1.16.10...v1.16.11) (2023-09-03)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.10](https://github.com/fast-crud/fast-crud/compare/v1.16.9...v1.16.10) (2023-09-03)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.9](https://github.com/fast-crud/fast-crud/compare/v1.16.8...v1.16.9) (2023-09-03)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 表单下所有组件优化为宽度100% ([da38460](https://github.com/fast-crud/fast-crud/commit/da384605f9c6bfc26359a369613dce4f48a3ba64))
|
|
||||||
|
|
||||||
## [1.16.8](https://github.com/fast-crud/fast-crud/compare/v1.16.7...v1.16.8) (2023-09-03)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 表单labelWidth演示 ([72f5372](https://github.com/fast-crud/fast-crud/commit/72f5372948f9aefebb0aba8671c277e8d80566bd))
|
|
||||||
* 翻页后自动滚动到顶部 ([a6e5f67](https://github.com/fast-crud/fast-crud/commit/a6e5f6740a59780995283c7d787864fdd65f0d4b))
|
|
||||||
|
|
||||||
## [1.16.7](https://github.com/fast-crud/fast-crud/compare/v1.16.6...v1.16.7) (2023-08-21)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.6](https://github.com/fast-crud/fast-crud/compare/v1.16.5...v1.16.6) (2023-08-21)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.5](https://github.com/fast-crud/fast-crud/compare/v1.16.4...v1.16.5) (2023-08-20)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.4](https://github.com/fast-crud/fast-crud/compare/v1.16.3...v1.16.4) (2023-08-18)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.3](https://github.com/fast-crud/fast-crud/compare/v1.16.2...v1.16.3) (2023-08-18)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* fs-button增加buttonProps参数,当fs-button的属性与x-button属性名重复时使用 ([5ca5333](https://github.com/fast-crud/fast-crud/commit/5ca53330f8bcf8d7acf4eb921aa92b83c41de52a))
|
|
||||||
|
|
||||||
## [1.16.2](https://github.com/fast-crud/fast-crud/compare/v1.16.1...v1.16.2) (2023-08-10)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.16.1](https://github.com/fast-crud/fast-crud/compare/v1.16.0...v1.16.1) (2023-08-09)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
# [1.16.0](https://github.com/fast-crud/fast-crud/compare/v1.15.1...v1.16.0) (2023-08-07)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
## [1.15.1](https://github.com/fast-crud/fast-crud/compare/v1.15.0...v1.15.1) (2023-08-05)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv4
|
|
||||||
|
|
||||||
# [1.15.0](https://github.com/fast-crud/fast-crud/compare/v1.14.7...v1.15.0) (2023-08-05)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* antdv4 支持 ([1935614](https://github.com/fast-crud/fast-crud/commit/19356142cda925d1248fe7c84c18cb8324ce5f70))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 适配antdv4样式 ([1108f58](https://github.com/fast-crud/fast-crud/commit/1108f5874a5369cbdb6f015264327ea8a879da61))
|
|
||||||
|
|
||||||
## [1.14.7](https://github.com/fast-crud/fast-crud/compare/v1.14.6...v1.14.7) (2023-07-24)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.14.6](https://github.com/fast-crud/fast-crud/compare/v1.14.5...v1.14.6) (2023-07-23)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复element 版 search select组件右边超出显示的问题 ([ff11cf4](https://github.com/fast-crud/fast-crud/commit/ff11cf4f9c6ac63d997b5cad2067123c01cd299b))
|
|
||||||
|
|
||||||
## [1.14.5](https://github.com/fast-crud/fast-crud/compare/v1.14.4...v1.14.5) (2023-07-04)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复search.value第一次查询无效的bug ([d9a907a](https://github.com/fast-crud/fast-crud/commit/d9a907a477bae66662a8a8720a24ab3506772d30))
|
|
||||||
|
|
||||||
## [1.14.4](https://github.com/fast-crud/fast-crud/compare/v1.14.3...v1.14.4) (2023-07-02)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.14.3](https://github.com/fast-crud/fast-crud/compare/v1.14.2...v1.14.3) (2023-07-02)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.14.2](https://github.com/fast-crud/fast-crud/compare/v1.14.1...v1.14.2) (2023-07-02)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复多选导出csv,导致表格错位的bug ([4e0bf5b](https://github.com/fast-crud/fast-crud/commit/4e0bf5bae3bd39fd1654c5cf10991039eacf1acc))
|
|
||||||
* 修复某些情况下fs-icon spin失效的bug ([2499a33](https://github.com/fast-crud/fast-crud/commit/2499a338def7436356c91a9b547e570c4204286d))
|
|
||||||
* 修复行编辑模式下,render、conditionalRender无效的bug ([403fedc](https://github.com/fast-crud/fast-crud/commit/403fedc6e22817e33a1f4ac316a016e570127aa8))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 导出重构 ([e99dc7b](https://github.com/fast-crud/fast-crud/commit/e99dc7bb6b24d4456fc524a04e8787e16b07511e))
|
|
||||||
* export 功能 ([2accdba](https://github.com/fast-crud/fast-crud/commit/2accdba5d087c01a87c6fd20b98c6510d0038f9d))
|
|
||||||
|
|
||||||
## [1.14.1](https://github.com/fast-crud/fast-crud/compare/v1.14.0...v1.14.1) (2023-06-16)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
# [1.14.0](https://github.com/fast-crud/fast-crud/compare/v1.13.12...v1.14.0) (2023-06-09)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* crudBinding.value.table.columns由array改成map ([8f89d2b](https://github.com/fast-crud/fast-crud/commit/8f89d2b26e12be0b3bcec2da8b4d7a2942395e8e))
|
|
||||||
|
|
||||||
## [1.13.12](https://github.com/fast-crud/fast-crud/compare/v1.13.11...v1.13.12) (2023-06-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.13.11](https://github.com/fast-crud/fast-crud/compare/v1.13.10...v1.13.11) (2023-06-08)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.13.10](https://github.com/fast-crud/fast-crud/compare/v1.13.9...v1.13.10) (2023-05-31)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.13.9](https://github.com/fast-crud/fast-crud/compare/v1.13.8...v1.13.9) (2023-05-31)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复antdv文件上传限制数量的bug ([8b14ba3](https://github.com/fast-crud/fast-crud/commit/8b14ba3a45f90a11222cc751b2ca173e212bc666))
|
|
||||||
|
|
||||||
## [1.13.8](https://github.com/fast-crud/fast-crud/compare/v1.13.7...v1.13.8) (2023-05-22)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.13.7](https://github.com/fast-crud/fast-crud/compare/v1.13.6...v1.13.7) (2023-05-19)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复rowhandle 排列不整齐的问题 ([ff644b1](https://github.com/fast-crud/fast-crud/commit/ff644b11b91c58295692fc0874dc4a3d743eb6df))
|
|
||||||
|
|
||||||
## [1.13.6](https://github.com/fast-crud/fast-crud/compare/v1.13.5...v1.13.6) (2023-05-13)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.13.5](https://github.com/fast-crud/fast-crud/compare/v1.13.4...v1.13.5) (2023-05-13)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.13.4](https://github.com/fast-crud/fast-crud/compare/v1.13.3...v1.13.4) (2023-05-06)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 1.13.3 ([451bd53](https://github.com/fast-crud/fast-crud/commit/451bd5390ce88fcbb875d39a39c88b3226f46b4e))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* naiveui sortable示例完善 ([dcd9e5b](https://github.com/fast-crud/fast-crud/commit/dcd9e5b04df7bda352878f4f1e30874ab9a6f452))
|
|
||||||
|
|
||||||
## [1.13.3](https://github.com/fast-crud/fast-crud/compare/v1.13.2...v1.13.3) (2023-05-04)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 升级cos sdk,修复windows报毒问题 ([352f8df](https://github.com/fast-crud/fast-crud/commit/352f8df76dfe093dd29c8778f3b33e3e3775b902))
|
|
||||||
|
|
||||||
## [1.13.2](https://github.com/fast-crud/fast-crud/compare/v1.13.1...v1.13.2) (2023-04-20)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* dict ts缺少cloneable参数 ([ab9528d](https://github.com/fast-crud/fast-crud/commit/ab9528d7ae2ab782cccc89d7530a22faa981ee74))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 优化fs-images-format 加载失败时的显示 ([7df6eab](https://github.com/fast-crud/fast-crud/commit/7df6eab4d653409de442eeef933177906a2ffc70))
|
|
||||||
|
|
||||||
## [1.13.1](https://github.com/fast-crud/fast-crud/compare/v1.13.0...v1.13.1) (2023-04-10)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
# [1.13.0](https://github.com/fast-crud/fast-crud/compare/v1.12.2...v1.13.0) (2023-04-07)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.12.2](https://github.com/fast-crud/fast-crud/compare/v1.12.1...v1.12.2) (2023-04-06)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.12.1](https://github.com/fast-crud/fast-crud/compare/v1.12.0...v1.12.1) (2023-04-04)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 增加自定义组件示例 ([c1f5b40](https://github.com/fast-crud/fast-crud/commit/c1f5b407d6137a0f5cbedb1ec2a56a18140e77a1))
|
|
||||||
|
|
||||||
# [1.12.0](https://github.com/fast-crud/fast-crud/compare/v1.11.10...v1.12.0) (2023-03-31)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 优化多行查询示例 ([95fa427](https://github.com/fast-crud/fast-crud/commit/95fa427043b29ef9590ce75fe91df9d5d686b196))
|
|
||||||
|
|
||||||
## [1.11.10](https://github.com/fast-crud/fast-crud/compare/v1.11.9...v1.11.10) (2023-03-29)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.9](https://github.com/fast-crud/fast-crud/compare/v1.11.8...v1.11.9) (2023-03-28)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.8](https://github.com/fast-crud/fast-crud/compare/v1.11.7...v1.11.8) (2023-03-24)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复当limit=1时,上传文件删光后,再选择文件上传第一次无效的bug ([d0a1ed9](https://github.com/fast-crud/fast-crud/commit/d0a1ed9c8a730d5eea19dc61f0dd6cf4031db1c3))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 优化翻页性能 ([d0a1db7](https://github.com/fast-crud/fast-crud/commit/d0a1db7bda08b49226739bba38e28b38c60c2b65))
|
|
||||||
|
|
||||||
## [1.11.7](https://github.com/fast-crud/fast-crud/compare/v1.11.6...v1.11.7) (2023-03-22)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.6](https://github.com/fast-crud/fast-crud/compare/v1.11.5...v1.11.6) (2023-03-22)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.5](https://github.com/fast-crud/fast-crud/compare/v1.11.4...v1.11.5) (2023-03-22)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.4](https://github.com/fast-crud/fast-crud/compare/v1.11.3...v1.11.4) (2023-03-22)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* doRefresh增加参数 ([a585604](https://github.com/fast-crud/fast-crud/commit/a5856045380f4a3fe2e657fd2ace1aea3473c6d7))
|
|
||||||
|
|
||||||
## [1.11.3](https://github.com/fast-crud/fast-crud/compare/v1.11.2...v1.11.3) (2023-03-21)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.2](https://github.com/fast-crud/fast-crud/compare/v1.11.1...v1.11.2) (2023-03-21)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.11.1](https://github.com/fast-crud/fast-crud/compare/v1.11.0...v1.11.1) (2023-03-17)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
# [1.11.0](https://github.com/fast-crud/fast-crud/compare/v1.10.0...v1.11.0) (2023-03-16)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 修复wangeditor无法上传视频的bug ([53ee51e](https://github.com/fast-crud/fast-crud/commit/53ee51e901956da9596600235632545bcf98746e))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 全面ts化 ([168d3a2](https://github.com/fast-crud/fast-crud/commit/168d3a240eb67548195c31a5fa4cb5aedb8a410c))
|
|
||||||
|
|
||||||
# [1.10.0](https://github.com/fast-crud/fast-crud/compare/v1.9.2...v1.10.0) (2023-03-11)
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* 行编辑支持多级表头 ([a547c99](https://github.com/fast-crud/fast-crud/commit/a547c99250f2d00b9d91c326364ccb81415c2772))
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* fs-form-wrapper支持多实例 ([023cc1d](https://github.com/fast-crud/fast-crud/commit/023cc1d425d5b1fa618a3d13fe5c88c81671524d))
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* 完善文档,完善部分types ([8fff02d](https://github.com/fast-crud/fast-crud/commit/8fff02d758530bbb1212d7475dc94bc8b562ef97))
|
|
||||||
|
|
||||||
* 优化d.ts类型 ([7a51aac](https://github.com/fast-crud/fast-crud/commit/7a51aace532ed6692f28a53332a2103a74f5827a))
|
|
||||||
|
|
||||||
* 增加s3示例 ([9060b03](https://github.com/fast-crud/fast-crud/commit/9060b036ce9e36ef8f2ddc50b1362682c7d3aa7f))
|
|
||||||
|
|
||||||
## [1.9.2](https://github.com/fast-crud/fast-crud/compare/v1.9.1...v1.9.2) (2023-03-01)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [1.9.1](https://github.com/fast-crud/fast-crud/compare/v1.9.0...v1.9.1) (2023-03-01)
|
|
||||||
|
|
||||||
**Note:** Version bump only for package @fast-crud/fs-admin-antdv
|
|
||||||
|
|
||||||
## [0.10.5](https://github.com/fast-crud/fast-crud/compare/v0.10.4...v0.10.5) (2021-07-01)
|
|
||||||
|
|
||||||
### Performance Improvements
|
|
||||||
|
|
||||||
* fs-admin 与crud demo ([4e6b20f](https://github.com/fast-crud/fast-crud/commit/4e6b20fe19434460853841f371b9fd5f16e5e2d3))
|
|
||||||
|
|
||||||
* fs-admin纳入子模块 ([2940d30](https://github.com/fast-crud/fast-crud/commit/2940d30f419bf4bde1e8e791f1fbdb9184818285))
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# fs-admin-antdv
|
|
||||||
|
|
||||||
基于vue3、antdv 的 admin管理后台脚手架
|
|
||||||
更多信息请参考: [fast-crud](https://github.com/fast-crud/fast-crud)
|
|
||||||
# server端
|
|
||||||
|
|
||||||
## fs-server-js
|
|
||||||
https://github.com/fast-crud/fs-server-js
|
|
||||||
|
|
||||||
# 其他相关项目
|
|
||||||
* [fast-crud](https://github.com/fast-crud/fast-crud) crud主项目
|
|
||||||
* [fs-admin-element](https://github.com/fast-crud/fs-admin-element) element版示例
|
|
||||||
* [fs-admin-naive](https://github.com/fast-crud/fs-admin-naive-ui) naive版示例
|
|
||||||
* [fs-in-vben-starter](https://github.com/fast-crud/fs-in-vben-starter) vben示例
|
|
||||||
|
|
||||||
# build
|
|
||||||
|
|
||||||
```sh
|
|
||||||
set NODE_OPTIONS=--max-old-space-size=32768 && npm run build
|
|
||||||
```
|
|
||||||
# 感谢
|
|
||||||
|
|
||||||
### 依赖
|
|
||||||
* [vue](https://github.com/vuejs/vue-next)
|
|
||||||
* [vue-router](https://github.com/vuejs/vue-router-next)
|
|
||||||
* [antdv 2x](https://github.com/vueComponent/ant-design-vue)
|
|
||||||
* [vitejs](https://github.com/vitejs/vite)
|
|
||||||
* [pinia](https://github.com/posva/pinia)
|
|
||||||
* [purge-icons](https://github.com/antfu/purge-icons)
|
|
||||||
|
|
||||||
### 参考如下项目
|
|
||||||
* [d2-admin](https://github.com/d2-projects/d2-admin)
|
|
||||||
* [antdv-pro](https://github.com/vueComponent/ant-design-vue-pro)
|
|
||||||
* [vben-admin](https://github.com/anncwb/vue-vben-admin)
|
|
||||||
|
|
||||||
感谢这些优秀的项目
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// import { getThemeVariables } from "ant-design-vue/dist/theme";
|
|
||||||
// import path from "path";
|
|
||||||
// const resolve = path.resolve;
|
|
||||||
export function generateModifyVars(dark = false) {
|
|
||||||
//const modifyVars = getThemeVariables({ dark });
|
|
||||||
// const vars = `${resolve("src/style/theme/index.less")}`;
|
|
||||||
return {
|
|
||||||
//...modifyVars
|
|
||||||
// hack: `true; @import (reference) "${vars}";`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
import path from "node:path";
|
|
||||||
|
|
||||||
import { addDynamicIconSelectors } from "@iconify/tailwind";
|
|
||||||
import { getPackagesSync } from "@manypkg/get-packages";
|
|
||||||
import typographyPlugin from "@tailwindcss/typography";
|
|
||||||
import animate from "tailwindcss-animate";
|
|
||||||
|
|
||||||
import { enterAnimationPlugin } from "./plugins/entry.mjs";
|
|
||||||
|
|
||||||
// import defaultTheme from 'tailwindcss/defaultTheme';
|
|
||||||
|
|
||||||
const { packages } = getPackagesSync(process.cwd());
|
|
||||||
|
|
||||||
const tailwindPackages = [];
|
|
||||||
|
|
||||||
packages.forEach((pkg) => {
|
|
||||||
// apps目录下和 @vben-core/tailwind-ui 包需要使用到 tailwindcss ui
|
|
||||||
// if (fs.existsSync(path.join(pkg.dir, 'tailwind.config.mjs'))) {
|
|
||||||
tailwindPackages.push(pkg.dir);
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
|
|
||||||
const shadcnUiColors = {
|
|
||||||
accent: {
|
|
||||||
DEFAULT: "hsl(var(--accent))",
|
|
||||||
foreground: "hsl(var(--accent-foreground))",
|
|
||||||
hover: "hsl(var(--accent-hover))",
|
|
||||||
lighter: "has(val(--accent-lighter))"
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
deep: "hsl(var(--background-deep))",
|
|
||||||
DEFAULT: "hsl(var(--background))"
|
|
||||||
},
|
|
||||||
border: {
|
|
||||||
DEFAULT: "hsl(var(--border))"
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
DEFAULT: "hsl(var(--card))",
|
|
||||||
foreground: "hsl(var(--card-foreground))"
|
|
||||||
},
|
|
||||||
destructive: {
|
|
||||||
...createColorsPalette("destructive"),
|
|
||||||
DEFAULT: "hsl(var(--destructive))"
|
|
||||||
},
|
|
||||||
|
|
||||||
foreground: {
|
|
||||||
DEFAULT: "hsl(var(--foreground))"
|
|
||||||
},
|
|
||||||
|
|
||||||
input: {
|
|
||||||
background: "hsl(var(--input-background))",
|
|
||||||
DEFAULT: "hsl(var(--input))"
|
|
||||||
},
|
|
||||||
muted: {
|
|
||||||
DEFAULT: "hsl(var(--muted))",
|
|
||||||
foreground: "hsl(var(--muted-foreground))"
|
|
||||||
},
|
|
||||||
popover: {
|
|
||||||
DEFAULT: "hsl(var(--popover))",
|
|
||||||
foreground: "hsl(var(--popover-foreground))"
|
|
||||||
},
|
|
||||||
primary: {
|
|
||||||
...createColorsPalette("primary"),
|
|
||||||
DEFAULT: "hsl(var(--primary))"
|
|
||||||
},
|
|
||||||
|
|
||||||
ring: "hsl(var(--ring))",
|
|
||||||
secondary: {
|
|
||||||
DEFAULT: "hsl(var(--secondary))",
|
|
||||||
desc: "hsl(var(--secondary-desc))",
|
|
||||||
foreground: "hsl(var(--secondary-foreground))"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const customColors = {
|
|
||||||
green: {
|
|
||||||
...createColorsPalette("green"),
|
|
||||||
foreground: "hsl(var(--success-foreground))"
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
DEFAULT: "hsl(var(--header))"
|
|
||||||
},
|
|
||||||
heavy: {
|
|
||||||
DEFAULT: "hsl(var(--heavy))",
|
|
||||||
foreground: "hsl(var(--heavy-foreground))"
|
|
||||||
},
|
|
||||||
main: {
|
|
||||||
DEFAULT: "hsl(var(--main))"
|
|
||||||
},
|
|
||||||
overlay: {
|
|
||||||
content: "hsl(var(--overlay-content))",
|
|
||||||
DEFAULT: "hsl(var(--overlay))"
|
|
||||||
},
|
|
||||||
red: {
|
|
||||||
...createColorsPalette("red"),
|
|
||||||
foreground: "hsl(var(--destructive-foreground))"
|
|
||||||
},
|
|
||||||
sidebar: {
|
|
||||||
deep: "hsl(var(--sidebar-deep))",
|
|
||||||
DEFAULT: "hsl(var(--sidebar))"
|
|
||||||
},
|
|
||||||
success: {
|
|
||||||
...createColorsPalette("success"),
|
|
||||||
DEFAULT: "hsl(var(--success))"
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
...createColorsPalette("warning"),
|
|
||||||
DEFAULT: "hsl(var(--warning))"
|
|
||||||
},
|
|
||||||
yellow: {
|
|
||||||
...createColorsPalette("yellow"),
|
|
||||||
foreground: "hsl(var(--warning-foreground))"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: ["./index.html", ...tailwindPackages.map((item) => path.join(item, "src/**/*.{vue,js,ts,jsx,tsx,svelte,astro,html}"))],
|
|
||||||
darkMode: "selector",
|
|
||||||
plugins: [animate, typographyPlugin, addDynamicIconSelectors(), enterAnimationPlugin],
|
|
||||||
prefix: "",
|
|
||||||
theme: {
|
|
||||||
container: {
|
|
||||||
center: true,
|
|
||||||
padding: "2rem",
|
|
||||||
screens: {
|
|
||||||
"2xl": "1400px"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extend: {
|
|
||||||
animation: {
|
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
|
||||||
"collapsible-down": "collapsible-down 0.2s ease-in-out",
|
|
||||||
"collapsible-up": "collapsible-up 0.2s ease-in-out",
|
|
||||||
float: "float 5s linear 0ms infinite"
|
|
||||||
},
|
|
||||||
|
|
||||||
animationDuration: {
|
|
||||||
2000: "2000ms",
|
|
||||||
3000: "3000ms"
|
|
||||||
},
|
|
||||||
borderRadius: {
|
|
||||||
lg: "var(--radius)",
|
|
||||||
md: "calc(var(--radius) - 2px)",
|
|
||||||
sm: "calc(var(--radius) - 4px)",
|
|
||||||
xl: "calc(var(--radius) + 4px)"
|
|
||||||
},
|
|
||||||
boxShadow: {
|
|
||||||
float: `0 6px 16px 0 rgb(0 0 0 / 8%),
|
|
||||||
0 3px 6px -4px rgb(0 0 0 / 12%),
|
|
||||||
0 9px 28px 8px rgb(0 0 0 / 5%)`
|
|
||||||
},
|
|
||||||
colors: {
|
|
||||||
...customColors,
|
|
||||||
...shadcnUiColors
|
|
||||||
},
|
|
||||||
fontFamily: {
|
|
||||||
sans: [
|
|
||||||
"var(--font-family)"
|
|
||||||
// ...defaultTheme.fontFamily.sans
|
|
||||||
]
|
|
||||||
},
|
|
||||||
keyframes: {
|
|
||||||
"accordion-down": {
|
|
||||||
from: { height: "0" },
|
|
||||||
to: { height: "var(--radix-accordion-content-height)" }
|
|
||||||
},
|
|
||||||
"accordion-up": {
|
|
||||||
from: { height: "var(--radix-accordion-content-height)" },
|
|
||||||
to: { height: "0" }
|
|
||||||
},
|
|
||||||
"collapsible-down": {
|
|
||||||
from: { height: "0" },
|
|
||||||
to: { height: "var(--radix-collapsible-content-height)" }
|
|
||||||
},
|
|
||||||
"collapsible-up": {
|
|
||||||
from: { height: "var(--radix-collapsible-content-height)" },
|
|
||||||
to: { height: "0" }
|
|
||||||
},
|
|
||||||
float: {
|
|
||||||
"0%": { transform: "translateY(0)" },
|
|
||||||
"50%": { transform: "translateY(-20px)" },
|
|
||||||
"100%": { transform: "translateY(0)" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
zIndex: {
|
|
||||||
100: "100",
|
|
||||||
1000: "1000"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
safelist: ["dark"]
|
|
||||||
};
|
|
||||||
|
|
||||||
function createColorsPalette(name) {
|
|
||||||
// backgroundLightest: '#EFF6FF', // Tailwind CSS 默认的 `blue-50`
|
|
||||||
// backgroundLighter: '#DBEAFE', // Tailwind CSS 默认的 `blue-100`
|
|
||||||
// backgroundLight: '#BFDBFE', // Tailwind CSS 默认的 `blue-200`
|
|
||||||
// borderLight: '#93C5FD', // Tailwind CSS 默认的 `blue-300`
|
|
||||||
// border: '#60A5FA', // Tailwind CSS 默认的 `blue-400`
|
|
||||||
// main: '#3B82F6', // Tailwind CSS 默认的 `blue-500`
|
|
||||||
// hover: '#2563EB', // Tailwind CSS 默认的 `blue-600`
|
|
||||||
// active: '#1D4ED8', // Tailwind CSS 默认的 `blue-700`
|
|
||||||
// backgroundDark: '#1E40AF', // Tailwind CSS 默认的 `blue-800`
|
|
||||||
// backgroundDarker: '#1E3A8A', // Tailwind CSS 默认的 `blue-900`
|
|
||||||
// backgroundDarkest: '#172554', // Tailwind CSS 默认的 `blue-950`
|
|
||||||
|
|
||||||
// • backgroundLightest (#EFF6FF): 适用于最浅的背景色,可能用于非常轻微的阴影或卡片的背景。
|
|
||||||
// • backgroundLighter (#DBEAFE): 适用于略浅的背景色,通常用于次要背景或略浅的区域。
|
|
||||||
// • backgroundLight (#BFDBFE): 适用于浅色背景,可能用于输入框或表单区域的背景。
|
|
||||||
// • borderLight (#93C5FD): 适用于浅色边框,可能用于输入框或卡片的边框。
|
|
||||||
// • border (#60A5FA): 适用于普通边框,可能用于按钮或卡片的边框。
|
|
||||||
// • main (#3B82F6): 适用于主要的主题色,通常用于按钮、链接或主要的强调色。
|
|
||||||
// • hover (#2563EB): 适用于鼠标悬停状态下的颜色,例如按钮悬停时的背景色或边框色。
|
|
||||||
// • active (#1D4ED8): 适用于激活状态下的颜色,例如按钮按下时的背景色或边框色。
|
|
||||||
// • backgroundDark (#1E40AF): 适用于深色背景,可能用于主要按钮或深色卡片背景。
|
|
||||||
// • backgroundDarker (#1E3A8A): 适用于更深的背景,通常用于头部导航栏或页脚。
|
|
||||||
// • backgroundDarkest (#172554): 适用于最深的背景,可能用于非常深色的区域或极端对比色。
|
|
||||||
|
|
||||||
return {
|
|
||||||
50: `hsl(var(--${name}-50))`,
|
|
||||||
100: `hsl(var(--${name}-100))`,
|
|
||||||
200: `hsl(var(--${name}-200))`,
|
|
||||||
300: `hsl(var(--${name}-300))`,
|
|
||||||
400: `hsl(var(--${name}-400))`,
|
|
||||||
500: `hsl(var(--${name}-500))`,
|
|
||||||
600: `hsl(var(--${name}-600))`,
|
|
||||||
700: `hsl(var(--${name}-700))`,
|
|
||||||
// 800: `hsl(var(--${name}-800))`,
|
|
||||||
// 900: `hsl(var(--${name}-900))`,
|
|
||||||
// 950: `hsl(var(--${name}-950))`,
|
|
||||||
// 激活状态下的颜色,适用于按钮按下时的背景色或边框色。
|
|
||||||
active: `hsl(var(--${name}-700))`,
|
|
||||||
// 浅色背景,适用于输入框或表单区域的背景。
|
|
||||||
"background-light": `hsl(var(--${name}-200))`,
|
|
||||||
// 适用于略浅的背景色,通常用于次要背景或略浅的区域。
|
|
||||||
"background-lighter": `hsl(var(--${name}-100))`,
|
|
||||||
// 最浅的背景色,适用于非常轻微的阴影或卡片的背景。
|
|
||||||
"background-lightest": `hsl(var(--${name}-50))`,
|
|
||||||
// 适用于普通边框,可能用于按钮或卡片的边框。
|
|
||||||
border: `hsl(var(--${name}-400))`,
|
|
||||||
// 浅色边框,适用于输入框或卡片的边框。
|
|
||||||
"border-light": `hsl(var(--${name}-300))`,
|
|
||||||
foreground: `hsl(var(--${name}-foreground))`,
|
|
||||||
// 鼠标悬停状态下的颜色,适用于按钮悬停时的背景色或边框色。
|
|
||||||
hover: `hsl(var(--${name}-600))`,
|
|
||||||
// 主色文本
|
|
||||||
text: `hsl(var(--${name}-500))`,
|
|
||||||
// 主色文本激活态
|
|
||||||
"text-active": `hsl(var(--${name}-700))`,
|
|
||||||
// 主色文本悬浮态
|
|
||||||
"text-hover": `hsl(var(--${name}-600))`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import plugin from "tailwindcss/plugin.js";
|
|
||||||
|
|
||||||
const enterAnimationPlugin = plugin(({ addUtilities }) => {
|
|
||||||
const maxChild = 5;
|
|
||||||
const utilities = {};
|
|
||||||
for (let i = 1; i <= maxChild; i++) {
|
|
||||||
const baseDelay = 0.1;
|
|
||||||
const delay = `${baseDelay * i}s`;
|
|
||||||
|
|
||||||
utilities[`.enter-x:nth-child(${i})`] = {
|
|
||||||
animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,
|
|
||||||
opacity: "0",
|
|
||||||
transform: `translateX(50px)`
|
|
||||||
};
|
|
||||||
|
|
||||||
utilities[`.enter-y:nth-child(${i})`] = {
|
|
||||||
animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,
|
|
||||||
opacity: "0",
|
|
||||||
transform: `translateY(50px)`
|
|
||||||
};
|
|
||||||
|
|
||||||
utilities[`.-enter-x:nth-child(${i})`] = {
|
|
||||||
animation: `enter-x-animation 0.3s ease-in-out ${delay} forwards`,
|
|
||||||
opacity: "0",
|
|
||||||
transform: `translateX(-50px)`
|
|
||||||
};
|
|
||||||
|
|
||||||
utilities[`.-enter-y:nth-child(${i})`] = {
|
|
||||||
animation: `enter-y-animation 0.3s ease-in-out ${delay} forwards`,
|
|
||||||
opacity: "0",
|
|
||||||
transform: `translateY(-50px)`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加动画关键帧
|
|
||||||
addUtilities(utilities);
|
|
||||||
addUtilities({
|
|
||||||
"@keyframes enter-x-animation": {
|
|
||||||
to: {
|
|
||||||
opacity: "1",
|
|
||||||
transform: "translateX(0)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@keyframes enter-y-animation": {
|
|
||||||
to: {
|
|
||||||
opacity: "1",
|
|
||||||
transform: "translateY(0)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export { enterAnimationPlugin };
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import config from ".";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
|
|
||||||
// Specifying the config is not necessary in most cases, but it is included
|
|
||||||
autoprefixer: {},
|
|
||||||
// 修复 element-plus 和 ant-design-vue 的样式和tailwindcss冲突问题
|
|
||||||
"postcss-antd-fixes": { prefixes: ["ant", "el"] },
|
|
||||||
"postcss-import": {},
|
|
||||||
"postcss-preset-env": {},
|
|
||||||
tailwindcss: { config },
|
|
||||||
"tailwindcss/nesting": {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { generate } from "@ant-design/colors";
|
|
||||||
|
|
||||||
export const primaryColor = "#1890ff";
|
|
||||||
|
|
||||||
export const darkMode = "light";
|
|
||||||
|
|
||||||
type Fn = (...arg: any) => any;
|
|
||||||
|
|
||||||
export interface GenerateColorsParams {
|
|
||||||
mixLighten: Fn;
|
|
||||||
mixDarken: Fn;
|
|
||||||
tinycolor: any;
|
|
||||||
color?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateAntColors(color: string) {
|
|
||||||
return generate(color, {
|
|
||||||
theme: "default"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getThemeColors(color?: string) {
|
|
||||||
const tc = color || primaryColor;
|
|
||||||
const colors = generateAntColors(tc);
|
|
||||||
const primary = colors[5];
|
|
||||||
const modeColors = generateAntColors(primary);
|
|
||||||
|
|
||||||
return [...colors, ...modeColors];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateColors({ color = primaryColor, mixLighten, mixDarken, tinycolor }: GenerateColorsParams) {
|
|
||||||
const arr = new Array(19).fill(0);
|
|
||||||
const lightens = arr.map((_t, i) => {
|
|
||||||
return mixLighten(color, i / 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
const darkens = arr.map((_t, i) => {
|
|
||||||
return mixDarken(color, i / 5);
|
|
||||||
});
|
|
||||||
|
|
||||||
const alphaColors = arr.map((_t, i) => {
|
|
||||||
return tinycolor(color)
|
|
||||||
.setAlpha(i / 20)
|
|
||||||
.toRgbString();
|
|
||||||
});
|
|
||||||
|
|
||||||
const shortAlphaColors = alphaColors.map((item) => item.replace(/\s/g, "").replace(/0\./g, "."));
|
|
||||||
|
|
||||||
const tinycolorLightens = arr
|
|
||||||
.map((_t, i) => {
|
|
||||||
return tinycolor(color)
|
|
||||||
.lighten(i * 5)
|
|
||||||
.toHexString();
|
|
||||||
})
|
|
||||||
.filter((item) => item !== "#ffffff");
|
|
||||||
|
|
||||||
const tinycolorDarkens = arr
|
|
||||||
.map((_t, i) => {
|
|
||||||
return tinycolor(color)
|
|
||||||
.darken(i * 5)
|
|
||||||
.toHexString();
|
|
||||||
})
|
|
||||||
.filter((item) => item !== "#000000");
|
|
||||||
return [
|
|
||||||
...lightens,
|
|
||||||
...darkens,
|
|
||||||
...alphaColors,
|
|
||||||
...shortAlphaColors,
|
|
||||||
...tinycolorDarkens,
|
|
||||||
...tinycolorLightens
|
|
||||||
].filter((item) => !item.includes("-"));
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
/**
|
|
||||||
* Vite plugin for website theme color switching
|
|
||||||
* https://github.com/anncwb/vite-plugin-theme
|
|
||||||
*/
|
|
||||||
import type { Plugin } from "vite";
|
|
||||||
import path from "path";
|
|
||||||
import { viteThemePlugin, mixLighten, mixDarken, tinycolor, antdDarkThemePlugin } from "vite-plugin-theme";
|
|
||||||
import { getThemeColors, generateColors } from "./theme-colors";
|
|
||||||
import { generateModifyVars } from "./modify-vars";
|
|
||||||
|
|
||||||
export function configThemePlugin(isBuild: boolean): Plugin[] {
|
|
||||||
const colors = generateColors({
|
|
||||||
mixDarken,
|
|
||||||
mixLighten,
|
|
||||||
tinycolor
|
|
||||||
});
|
|
||||||
const colorVariables = [...getThemeColors(), ...colors];
|
|
||||||
const plugin = [
|
|
||||||
viteThemePlugin({
|
|
||||||
// resolveSelector: (s) => {
|
|
||||||
// s = s.trim();
|
|
||||||
// switch (s) {
|
|
||||||
// case ".ant-steps-item-process .ant-steps-item-icon > .ant-steps-icon":
|
|
||||||
// return ".ant-steps-item-icon > .ant-steps-icon";
|
|
||||||
// case ".ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled)":
|
|
||||||
// case ".ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover":
|
|
||||||
// case ".ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active":
|
|
||||||
// return s;
|
|
||||||
// case ".ant-steps-item-icon > .ant-steps-icon":
|
|
||||||
// return s;
|
|
||||||
// }
|
|
||||||
// return `[data-theme] ${s}`;
|
|
||||||
// },
|
|
||||||
resolveSelector: (s) => {
|
|
||||||
s = s.trim();
|
|
||||||
if (s === ".ant-btn:hover,.ant-btn:focus") {
|
|
||||||
// console.log("ssss", s);
|
|
||||||
return ".theme-discard-xxxxxxx";
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
},
|
|
||||||
colorVariables
|
|
||||||
}),
|
|
||||||
antdDarkThemePlugin({
|
|
||||||
preloadFiles: [
|
|
||||||
path.resolve(process.cwd(), "node_modules/ant-design-vue/dist/antd.less"),
|
|
||||||
path.resolve(process.cwd(), "src/style/theme/index.less")
|
|
||||||
],
|
|
||||||
filter: (id) => (isBuild ? !id.endsWith("antd.less") : true),
|
|
||||||
// extractCss: false,
|
|
||||||
darkModifyVars: {
|
|
||||||
...generateModifyVars(true),
|
|
||||||
"text-color": "#c9d1d9",
|
|
||||||
"text-color-base": "#c9d1d9",
|
|
||||||
"component-background": "#151515",
|
|
||||||
// black: '#0e1117',
|
|
||||||
// #8b949e
|
|
||||||
"text-color-secondary": "#8b949e",
|
|
||||||
"border-color-base": "#303030",
|
|
||||||
// 'border-color-split': '#30363d',
|
|
||||||
"item-active-bg": "#111b26",
|
|
||||||
"app-content-background": "rgb(255 255 255 / 4%)"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
return (plugin as unknown) as Plugin[];
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" href="/logo.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title><%= title %></title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app">
|
|
||||||
<div class="fs-bootstrap">
|
|
||||||
<div class="fs-bootstrap__main">
|
|
||||||
<div class="fs-bootstrap__loading"></div>
|
|
||||||
</div>
|
|
||||||
<div class="fs-bootstrap__footer">
|
|
||||||
<a href="<%= projectPath %>" target="_blank">
|
|
||||||
<%= projectPath %>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<script type="module" src="/src/main.ts"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@fast-crud/fs-admin-antdv4",
|
|
||||||
"version": "1.26.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite",
|
|
||||||
"dev:pm": "vite --mode pm",
|
|
||||||
"dev:force": "vite --force",
|
|
||||||
"debug": "vite --mode debug --open",
|
|
||||||
"debug:pm": "vite --mode debugpm",
|
|
||||||
"debug:force": "vite --force --mode debug",
|
|
||||||
"build": "vite build ",
|
|
||||||
"serve": "vite preview",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"pretty-quick": "pretty-quick",
|
|
||||||
"lint-fix": "eslint --fix --ext .js --ext .jsx --ext .vue src/",
|
|
||||||
"upgrade": "yarn upgrade-interactive --latest",
|
|
||||||
"tsc": "vue-tsc --noEmit --skipLibCheck",
|
|
||||||
"circle:check": "pnpm dependency-cruise --validate --output-type err-html -f dependency-report.html src",
|
|
||||||
"afterPubPush": "git add . && git commit -m \"build: publish success\" && git push"
|
|
||||||
},
|
|
||||||
"author": "greper",
|
|
||||||
"license": "MIT",
|
|
||||||
"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",
|
|
||||||
"@ctrl/tinycolor": "^4.1.0",
|
|
||||||
"@fast-crud/editor-code": "^1.26.0",
|
|
||||||
"@fast-crud/fast-crud": "^1.26.0",
|
|
||||||
"@fast-crud/fast-extends": "^1.26.0",
|
|
||||||
"@fast-crud/ui-antdv4": "^1.26.0",
|
|
||||||
"@fast-crud/ui-interface": "^1.26.0",
|
|
||||||
"@iconify/tailwind": "^1.2.0",
|
|
||||||
"@iconify/vue": "^4.1.1",
|
|
||||||
"@manypkg/get-packages": "^2.2.2",
|
|
||||||
"@soerenmartius/vue3-clipboard": "^0.1.2",
|
|
||||||
"@tailwindcss/nesting": "0.0.0-insiders.565cd3e",
|
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"@tanstack/vue-store": "^0.7.0",
|
|
||||||
"@vee-validate/zod": "^4.15.0",
|
|
||||||
"@vue/shared": "^3.5.13",
|
|
||||||
"@vueuse/core": "^10.11.0",
|
|
||||||
"ant-design-vue": "^4.2.6",
|
|
||||||
"axios": "^1.6.8",
|
|
||||||
"axios-mock-adapter": "^1.22.0",
|
|
||||||
"base64-js": "^1.5.1",
|
|
||||||
"better-scroll": "^2.5.1",
|
|
||||||
"china-division": "^2.7.0",
|
|
||||||
"class-variance-authority": "^0.7.1",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"core-js": "^3.36.0",
|
|
||||||
"cos-js-sdk-v5": "^1.7.0",
|
|
||||||
"cropperjs": "^1.6.1",
|
|
||||||
"cssnano": "^7.0.6",
|
|
||||||
"dayjs": "^1.11.10",
|
|
||||||
"defu": "^6.1.4",
|
|
||||||
"highlight.js": "^11.9.0",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"lodash-es": "^4.17.21",
|
|
||||||
"lucide-vue-next": "^0.477.0",
|
|
||||||
"mitt": "^3.0.1",
|
|
||||||
"monaco-editor": "^0.52.2",
|
|
||||||
"monaco-yaml": "^5.3.1",
|
|
||||||
"nprogress": "^0.2.0",
|
|
||||||
"object-assign": "^4.1.1",
|
|
||||||
"pinia": "2.1.7",
|
|
||||||
"pinia-plugin-persistedstate": "^4.2.0",
|
|
||||||
"postcss-antd-fixes": "^0.2.0",
|
|
||||||
"postcss-import": "^16.1.0",
|
|
||||||
"postcss-preset-env": "^10.1.5",
|
|
||||||
"qiniu-js": "^3.4.2",
|
|
||||||
"radix-vue": "^1.9.16",
|
|
||||||
"sortablejs": "^1.15.3",
|
|
||||||
"tailwind-merge": "^3.0.2",
|
|
||||||
"tailwindcss-animate": "^1.0.7",
|
|
||||||
"theme-colors": "^0.1.0",
|
|
||||||
"vee-validate": "^4.15.0",
|
|
||||||
"vitest": "^0.34.6",
|
|
||||||
"vue": "^3.4.21",
|
|
||||||
"vue-cropperjs": "^5.0.0",
|
|
||||||
"vue-i18n": "^9.10.2",
|
|
||||||
"vue-router": "^4.3.0",
|
|
||||||
"vuedraggable": "^2.24.3",
|
|
||||||
"watermark-js-plus": "^1.5.8",
|
|
||||||
"yaml-language-server": "^1.17.0",
|
|
||||||
"zod": "^3.24.2",
|
|
||||||
"zod-defaults": "^0.1.3"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@rollup/plugin-commonjs": "^25.0.7",
|
|
||||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
|
||||||
"@types/chai": "^4.3.12",
|
|
||||||
"@types/lodash-es": "^4.17.12",
|
|
||||||
"@types/mocha": "^10.0.6",
|
|
||||||
"@types/node": "^20.11.28",
|
|
||||||
"@types/nprogress": "^0.2.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
|
||||||
"@vitejs/plugin-legacy": "^5.3.2",
|
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
|
||||||
"@vitejs/plugin-vue-jsx": "^3.1.0",
|
|
||||||
"@vue/compiler-sfc": "^3.4.21",
|
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
|
||||||
"@vue/test-utils": "^2.4.5",
|
|
||||||
"autoprefixer": "^10.4.20",
|
|
||||||
"caller-path": "^4.0.0",
|
|
||||||
"chai": "^5.1.0",
|
|
||||||
"dependency-cruiser": "^16.2.3",
|
|
||||||
"dot": "^1.1.3",
|
|
||||||
"eslint": "8.57.0",
|
|
||||||
"eslint-config-prettier": "^9.1.0",
|
|
||||||
"eslint-plugin-import": "^2.29.1",
|
|
||||||
"eslint-plugin-node": "^11.1.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
|
||||||
"eslint-plugin-vue": "^9.23.0",
|
|
||||||
"esno": "^4.7.0",
|
|
||||||
"husky": "^9.0.11",
|
|
||||||
"less": "^4.2.0",
|
|
||||||
"less-loader": "^12.2.0",
|
|
||||||
"lint-staged": "^15.2.2",
|
|
||||||
"postcss": "^8.4.35",
|
|
||||||
"prettier": "3.2.5",
|
|
||||||
"pretty-quick": "^4.0.0",
|
|
||||||
"rimraf": "^5.0.5",
|
|
||||||
"rollup": "^4.13.0",
|
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
|
||||||
"stylelint": "^16.2.1",
|
|
||||||
"stylelint-config-prettier": "^9.0.5",
|
|
||||||
"stylelint-order": "^6.0.4",
|
|
||||||
"tailwindcss": "^3.4.14",
|
|
||||||
"terser": "^5.29.2",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"tslint": "^6.1.3",
|
|
||||||
"typescript": "5.4.2",
|
|
||||||
"unplugin-vue-define-options": "^1.4.2",
|
|
||||||
"vite": "^5.1.6",
|
|
||||||
"vite-plugin-compression": "^0.5.1",
|
|
||||||
"vite-plugin-html": "^3.2.2",
|
|
||||||
"vite-plugin-theme": "^0.8.6",
|
|
||||||
"vite-plugin-windicss": "^1.9.3",
|
|
||||||
"vue-eslint-parser": "^9.4.2",
|
|
||||||
"vue-tsc": "^1.8.8",
|
|
||||||
"windicss": "^3.5.6"
|
|
||||||
},
|
|
||||||
"husky": {
|
|
||||||
"hooks": {
|
|
||||||
"pre-commit": "pretty-quick --staged"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"gitHead": "9c2162697f3affea22c9a8cbc0ca74f4034ab27e",
|
|
||||||
"vite": {
|
|
||||||
"optimizeDeps": {
|
|
||||||
"include": [
|
|
||||||
"@iconify/iconify"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import config from "./build/tailwind-config/index.mjs";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {}),
|
|
||||||
// Specifying the config is not necessary in most cases, but it is included
|
|
||||||
autoprefixer: {},
|
|
||||||
// 修复 element-plus 和 ant-design-vue 的样式和tailwindcss冲突问题
|
|
||||||
"postcss-antd-fixes": { prefixes: ["ant", "el"] },
|
|
||||||
"postcss-import": {},
|
|
||||||
"postcss-preset-env": {},
|
|
||||||
tailwindcss: { config },
|
|
||||||
"tailwindcss/nesting": {}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,106 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="210mm"
|
|
||||||
height="210mm"
|
|
||||||
viewBox="0 0 210 210"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
|
||||||
>
|
|
||||||
<g id="layer1" style="display:inline">
|
|
||||||
<path
|
|
||||||
style="fill:#002255;stroke:none;stroke-width:0.625348"
|
|
||||||
d="M 35.587501,69.766667 V 59.766664 h 70.000109 69.99991 v 10.000003 9.999997 H 105.58761 35.587501 Z"
|
|
||||||
id="path12" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-2"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="71.506088"
|
|
||||||
y="106.64581" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-8-8"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="107.42467"
|
|
||||||
y="106.64581" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-8-5-8"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="143.34325"
|
|
||||||
y="106.64581" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-2-4"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="71.506088"
|
|
||||||
y="129.82079" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-8-8-3"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="107.42467"
|
|
||||||
y="129.82079" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-8-5-8-2"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="143.34325"
|
|
||||||
y="129.82079" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-2-7"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="35.587502"
|
|
||||||
y="106.64581" />
|
|
||||||
<rect
|
|
||||||
style="fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-2-4-0"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="35.587502"
|
|
||||||
y="129.82079" />
|
|
||||||
<rect
|
|
||||||
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-2-9"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="71.506088"
|
|
||||||
y="82.941666" />
|
|
||||||
<rect
|
|
||||||
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-8-8-37"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="107.42467"
|
|
||||||
y="82.941666" />
|
|
||||||
<rect
|
|
||||||
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-8-5-8-4"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="143.34325"
|
|
||||||
y="82.941666" />
|
|
||||||
<rect
|
|
||||||
style="display:inline;fill:#2a7fff;fill-rule:evenodd;stroke-width:0.214311"
|
|
||||||
id="rect22-2-7-1"
|
|
||||||
width="32.244232"
|
|
||||||
height="20"
|
|
||||||
x="35.587502"
|
|
||||||
y="82.941666" />
|
|
||||||
</g>
|
|
||||||
<polygon
|
|
||||||
points="75.3,174.4 103.1,103.6 79.8,103.6 112.6,41.3 156.4,41.3 129.9,90.5 148.1,90.5 "
|
|
||||||
fill="#f6cc00"
|
|
||||||
id="polygon276"
|
|
||||||
transform="matrix(1.0930933,0,0,0.99853202,-17.517362,-0.52287941)" />
|
|
||||||
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user