Compare commits

..

48 Commits

Author SHA1 Message Date
xiaojunnuo
15467fc233 v0.1.10 2021-01-08 15:57:57 +08:00
xiaojunnuo
eec0fcdcf1 refactor: 1 2021-01-08 15:57:38 +08:00
xiaojunnuo
a41dee015e fix: 修复plugins为null的问题 2021-01-08 15:56:50 +08:00
xiaojunnuo
d1a74713ef v0.1.9 2021-01-08 15:49:48 +08:00
xiaojunnuo
813e9e71d7 refactor: 1 2021-01-08 15:49:16 +08:00
xiaojunnuo
3d08dce26e refactor: 1 2021-01-08 15:46:37 +08:00
xiaojunnuo
4739d75f4a refactor: 1 2021-01-08 15:45:31 +08:00
xiaojunnuo
30cd62664b refactor: 1 2021-01-08 15:39:23 +08:00
xiaojunnuo
ae6b0fb111 v0.1.8 2021-01-08 15:07:15 +08:00
xiaojunnuo
9ec48f6ab8 refactor: tke ingress 内网配置 2021-01-08 15:06:49 +08:00
xiaojunnuo
f68565f444 refactor: tke ingress 内网配置 2021-01-08 15:04:58 +08:00
xiaojunnuo
ce5aae3795 refactor: tke ingress 内网配置 2021-01-08 13:01:35 +08:00
xiaojunnuo
bd00c09da0 v0.1.7 2021-01-08 10:33:11 +08:00
xiaojunnuo
44326c3abe refactor: home环境变量 2021-01-08 10:32:58 +08:00
xiaojunnuo
2f3db7d982 refactor: home环境变量 2021-01-08 10:25:58 +08:00
xiaojunnuo
9e4e3044b4 refactor: 1 2021-01-08 09:41:36 +08:00
xiaojunnuo
23bf0d07f7 refactor: 1 2021-01-08 09:37:02 +08:00
xiaojunnuo
6e61f8bcfb refactor: 1 2021-01-08 09:36:07 +08:00
xiaojunnuo
c7b28feb07 fix: 1 2021-01-08 09:13:29 +08:00
xiaojunnuo
624eade9f2 refactor: 排除certd-run 2021-01-07 18:06:42 +08:00
xiaojunnuo
d727a77289 refactor: 优化 2021-01-07 18:05:38 +08:00
xiaojunnuo
c0c2cb328c refactor: 小修改 2021-01-06 23:55:26 +08:00
xiaojunnuo
8cc80deff8 perf: 抽取api 2021-01-06 23:52:10 +08:00
xiaojunnuo
5312c11472 perf: 抽取api 2021-01-06 23:29:10 +08:00
xiaojunnuo
f07ce6f47d perf: 优化证书申请过程 2021-01-06 22:44:04 +08:00
xiaojunnuo
259e797ea5 refactor: 排除dev依赖 2021-01-05 22:25:14 +08:00
xiaojunnuo
d77addd2dc refactor: rollup 2021-01-05 21:27:23 +08:00
xiaojunnuo
c2420edb5a refactor: wepack 2021-01-04 00:45:04 +08:00
xiaojunnuo
e1396bb107 refactor: version 2021-01-03 23:35:03 +08:00
xiaojunnuo
b0def03790 refactor: 修复几个小bug 2021-01-03 23:33:51 +08:00
xiaojunnuo
9f371df372 refactor: version 2021-01-03 21:58:05 +08:00
xiaojunnuo
986fd4b010 feat: webpack pre 2021-01-03 21:50:14 +08:00
xiaojunnuo
4cd7b02cb7 feat: 上传证书到服务器,执行远程脚本 2021-01-03 02:30:34 +08:00
xiaojunnuo
67bff28255 feat: 腾讯云证书tke ingress 2021-01-02 02:47:58 +08:00
xiaojunnuo
43e90503ca feat: 腾讯云证书clb支持与删除 2020-12-28 00:22:12 +08:00
xiaojunnuo
25dae3d1ec feat: 腾讯云cdn支持 2020-12-26 01:37:53 +08:00
xiaojunnuo
ec81572670 refactor: dnspod支持 2020-12-24 00:58:12 +08:00
xiaojunnuo
71f6d5769a refactor: dnspod支持 2020-12-24 00:49:31 +08:00
xiaojunnuo
49b96592a0 refactor: 优化 2020-12-22 22:47:07 +08:00
xiaojunnuo
af0875ac4c refactor: delete 2020-12-21 00:34:53 +08:00
xiaojunnuo
48192b9002 refactor: delete 2020-12-21 00:33:52 +08:00
xiaojunnuo
ca289d4604 refactor: ignore 2020-12-21 00:32:50 +08:00
xiaojunnuo
6529fd2fdb feat: 自动化流程 2020-12-21 00:32:17 +08:00
xiaojunnuo
e84f5e8b0a refactor: 111 2020-12-19 02:00:46 +08:00
xiaojunnuo
df9f561fd3 refactor: ``` 2020-12-19 01:57:52 +08:00
xiaojunnuo
06603759fd feat: deployFlow 2020-12-16 00:35:05 +08:00
xiaojunnuo
a4bd29e6bf feat: 多域名绑定一张证书 2020-12-15 01:09:41 +08:00
xiaojunnuo
458486dd6b feat: 自动申请证书 2020-12-13 23:06:17 +08:00
1374 changed files with 31823 additions and 83123 deletions

29
.gitignore vendored
View File

@@ -1,32 +1,11 @@
# 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
*.iml
out
gen
node_modules/
/test/*.private.*
/other/node-acme-client/.idea/
/*.log
/packages/ui/*/.idea
/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
/other/certd-run
/other/node-acme-client

0
README.md Normal file
View File

6
lerna.json Normal file
View File

@@ -0,0 +1,6 @@
{
"packages": [
"packages/*"
],
"version": "0.1.10"
}

15
package.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "root",
"private": true,
"type": "module",
"devDependencies": {
"lerna": "^3.18.4"
},
"scripts": {
"submodule": "git submodule update --init --recursive"
},
"license": "MIT",
"dependencies": {
"lodash": "^4.17.20"
}
}

14
packages/api/.eslintrc Normal file
View 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

File diff suppressed because it is too large Load Diff

28
packages/api/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "@certd/api",
"version": "0.1.7",
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"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"
}
}

View 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
}
}

View 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'

View 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)
}
}

View 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
}
}

View 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
}

View 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

View 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
}

View 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()

View File

@@ -0,0 +1,7 @@
export default function (timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, timeout)
})
}

14
packages/certd/.eslintrc Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}

2361
packages/certd/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
{
"name": "@certd/certd",
"version": "0.1.7",
"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.7",
"@certd/providers": "^0.1.7",
"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
View 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
}
}

147
packages/certd/src/index.js Normal file
View File

@@ -0,0 +1,147 @@
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 { DnsProviderFactory } from '@certd/providers'
import dayjs from 'dayjs'
import forge from 'node-forge'
const logger = util.logger
export class Certd {
constructor (options) {
this.options = options
this.email = options.cert.email
this.domains = options.cert.domains
this.domain = this.getMainDomain(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,
domain: this.domain
})
this.acme = new AcmeService(this.store)
}
getMainDomain (domains) {
if (domains == null) {
return null
}
if (typeof domains === 'string') {
return domains
}
if (domains.length > 0) {
return domains[0]
}
}
//
// buildDomainFileName (domains) {
// const domain = this.getMainDomain(domains)
// return domain.replace(/\*/g, '_')
// }
//
// buildCertDir (email, domains) {
// const domainFileName = this.buildDomainFileName(domains)
// return path.join(email, '/certs/', domainFileName)
// }
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 DnsProviderFactory.createByType(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.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
}
}
}

View File

@@ -0,0 +1,92 @@
import dayjs from 'dayjs'
export class CertStore {
constructor ({ store, email, domain }) {
this.store = store
this.email = email
this.domain = domain
this.safetyDomain = this.getSafetyDomain(this.domain)
this.certsRootPath = this.store.buildKey(this.email, 'certs')
this.currentRootPath = this.store.buildKey(this.certsRootPath, this.safetyDomain, 'current')
}
// getAccountConfig () {
// return this.store.get(this.accountConfigKey)
// }
//
// setAccountConfig (email, account) {
// return this.store.set(this.accountConfigKey, account)
// }
buildNewCertRootPath (dir) {
if (dir == null) {
dir = dayjs().format('YYYY.MM.DD.HHmmss')
}
return this.store.buildKey(this.certsRootPath, this.safetyDomain, 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)
}
}

View 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)
}
}

View 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
})
})

View 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
})
})

View 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\\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)
})
})

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,36 @@
{
"name": "@certd/executor",
"version": "0.1.10",
"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.7",
"@certd/certd": "^0.1.7",
"@certd/plugins": "^0.1.9",
"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
}

View 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()]
}

View File

@@ -0,0 +1,181 @@
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
}
}
}
export class Executor {
constructor () {
this.usePlugins(DefaultPlugins)
this.trace = new Trace()
}
use (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) {
for (const plugin of plugins) {
this.use(plugin)
}
}
}
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)
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)
// 运行部署任务
try {
await this.runDeploys({ options, cert, context, trace })
} finally {
await certd.certStore.setCurrentFile('context.json', JSON.stringify(context))
}
logger.info('任务完成')
trace.print()
const result = trace.get({ })
const returnData = {
cert,
context,
result
}
if (result.status === 'error') {
const err = new Error(result.remark)
err.data = returnData
}
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}】-----------`)
if (deploy.disabled === true) {
logger.info('此流程已被禁用,跳过')
logger.info('')
trace.set({ deployName, 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 })
}
trace.set({ deployName, value: { status: 'success', remark: '执行成功' } })
} catch (e) {
trace.set({ deployName, value: { status: 'error', remark: '执行失败:' + e.message } })
trace.set({ 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 traceStatus = trace.get({ deployName: deploy.deployName, taskName: taskName })
if (traceStatus?.status === 'success' && !options?.args?.forceRedeploy) {
logger.info(`----【${taskName}】已经执行完成,跳过此任务`)
trace.set({ deployName, taskName, value: { current: 'skip', status: 'success', remark: '已执行成功过,本次跳过' } })
return
}
logger.info(`----【${taskName}】开始执行`)
try {
await instance.execute({ cert, props: task.props, context })
trace.set({ deployName, taskName, value: { current: 'success', status: 'success', remark: '执行成功', time: dayjs().format() } })
} catch (e) {
trace.set({ deployName, taskName, value: { current: 'error', status: 'error', remark: e.message, time: dayjs().format() } })
throw e
}
logger.info(`----任务【${taskName}】执行完成`)
logger.info('')
}
}

View File

@@ -0,0 +1,82 @@
import { util } from '@certd/api'
import _ from 'lodash-es'
const logger = util.logger
export class Trace {
constructor (context) {
this.context = context
}
set ({ deployName, taskName, prop, value }) {
const key = this.buildTraceKey({ deployName, taskName, prop })
const oldValue = _.get(this.context, key) || {}
_.merge(oldValue, value)
_.set(this.context, key, oldValue)
}
get ({ deployName, taskName, prop }) {
return _.get(this.context, this.buildTraceKey({ deployName, taskName, prop }))
}
buildTraceKey ({ deployName, taskName, prop }) {
let key = '__trace__'
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({ })
// 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 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
}
}

View File

@@ -0,0 +1,31 @@
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(20000)
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(20000)
const executor = new Executor()
const options = createOptions()
const ret = await executor.run(options, { forceCert: true, forceDeploy: false })
expect(ret).ok
expect(ret.cert).ok
})
it('#forceDeploy', async function () {
this.timeout(20000)
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
})
})

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,14 @@
{
"extends": "standard",
"env": {
"mocha": true
},
"overrides": [
{
"files": ["*.test.js", "*.spec.js"],
"rules": {
"no-unused-expressions": "off"
}
}
]
}

3757
packages/plugins/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
{
"name": "@certd/plugins",
"version": "0.1.9",
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"dependencies": {
"@alicloud/pop-core": "^1.7.10",
"@certd/api": "^0.1.7",
"@certd/certd": "^0.1.7",
"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"
}

View 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)
}
}
}

View 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)
}
}

View 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
}
}

View 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)
}
}
}

View 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 }) {
}
}

View 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())
})
})
})
}
}

View 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 }) {
}
}

View 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
]

View 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, '_')
}
}

View 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
}
}

View 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)
}
}

View 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}`)
}
}
}

View 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
}
}

View 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
})
}
}

View 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)
})
})

View 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)
})
})

View 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)
})
})

View 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)
})
})

View 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

View 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)
})
})

View 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)
})
})

View 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)
})
})

View 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)
})
})

File diff suppressed because it is too large Load Diff

2922
packages/plugins/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,27 @@
{
"name": "@certd/providers",
"version": "0.1.7",
"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.7",
"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"
}
}

View File

@@ -0,0 +1,12 @@
import dnsProviders from './index.js'
export class DnsProviderFactory {
static createByType (type, options) {
try {
const Provider = dnsProviders[type]
return new Provider(options)
} catch (e) {
throw new Error('暂不支持此dnsProvider' + type, e)
}
}
}

View File

@@ -0,0 +1,108 @@
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) {
// e.code === 'DomainRecordDuplicate'
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
}
}

View 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?.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
}
}

View File

@@ -0,0 +1,6 @@
import { AliyunDnsProvider } from './impl/aliyun.js'
import { DnspodDnsProvider } from './impl/dnspod.js'
export default {
[AliyunDnsProvider.name()]: AliyunDnsProvider,
[DnspodDnsProvider.name()]: DnspodDnsProvider
}

View File

@@ -0,0 +1,4 @@
import dnsProviders from './dns-provider/index.js'
export { DnsProviderFactory } from './dns-provider/dns-provider-factory.js'
export default dnsProviders

View File

@@ -0,0 +1,34 @@
import pkg from 'chai'
import AliyunDnsProvider from '../../src/dns-provider/impl/aliyun.js'
import { createOptions } from '../../../../test/options.js'
import { Certd } from '../../src/index.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
})
})

View File

@@ -0,0 +1,24 @@
import pkg from 'chai'
import DnspodDnsProvider from '../../src/dns-provider/impl/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 })
})
})

View File

@@ -1,3 +0,0 @@
> 1%
last 2 versions
not dead

View File

@@ -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

View File

@@ -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 &copy; 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

View File

@@ -1,2 +0,0 @@
#登录与权限开启
VITE_APP_PM_ENABLED=false

View File

@@ -1,2 +0,0 @@
#登录与权限开启
VITE_APP_PM_ENABLED=true

View File

@@ -1,2 +0,0 @@
#登录与权限开启
VITE_APP_PM_ENABLED=true

View File

@@ -1,3 +0,0 @@
VITE_APP_API=http://www.docmirror.cn:7001/api
#登录与权限开启
VITE_APP_PM_ENABLED=true

View File

@@ -1,2 +0,0 @@
node_modules
.idea

View File

@@ -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",
// },
// ],
}
};

View File

@@ -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
- [ ] 其他:`请注明`

View File

@@ -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

View File

@@ -1,11 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
/stats.html
yarn.lock
.idea
/.idea/
yarn-error.log
vite-profile.cpuprofile

View File

@@ -1,2 +0,0 @@
node_modules
/stats.html

View File

@@ -1,2 +0,0 @@
link-workspace-packages=deep
prefer-workspace-packages=true

View File

@@ -1,5 +0,0 @@
{
"trailingComma": "none",
"printWidth": 220
}

View File

@@ -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
* 修复antdv4drawer弹窗过时的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-demoui-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))

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 fast-crud
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -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)
感谢这些优秀的项目

View File

@@ -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}";`
};
}

View File

@@ -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))`
};
}

View File

@@ -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 };

View File

@@ -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": {}
}
};

View File

@@ -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("-"));
}

View File

@@ -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[];
}

View File

@@ -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>

View File

@@ -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"
]
}
}
}

View File

@@ -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": {}
}
};

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