refactor: 重构优化

master
xiaojunnuo 2021-02-04 18:44:16 +08:00
parent a39dac4dbd
commit a25a15ca6e
59 changed files with 3903 additions and 967 deletions

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ node_modules/
/ui/*/node_modules /ui/*/node_modules
/packages/*/node_modules /packages/*/node_modules
/ui/certd-server/tmp/

View File

@ -5,8 +5,7 @@
"devDependencies": { "devDependencies": {
"lerna": "^3.18.4" "lerna": "^3.18.4"
}, },
"scripts": { "scripts": {},
},
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lodash-es": "^4.17.20" "lodash-es": "^4.17.20"

View File

@ -1,5 +1,5 @@
{ {
"name": "@certd/providers", "name": "@certd/dns-providers",
"version": "0.1.11", "version": "0.1.11",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,

View File

@ -0,0 +1,23 @@
{
"name": "@certd/access-providers",
"version": "0.1.11",
"description": "",
"main": "./src/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"type": "module",
"author": "Greper",
"license": "MIT",
"dependencies": {
"@certd/api": "^0.1.11",
"lodash-es": "^4.17.20"
},
"devDependencies": {
"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"
}
}

View File

@ -0,0 +1,18 @@
import _ from 'lodash-es'
import { AliyunAccessProvider } from './providers/aliyun.js'
import { DnspodAccessProvider } from './providers/dnspod.js'
import { TencentAccessProvider } from './providers/tencent.js'
import { accessProviderRegistry } from '@certd/api'
export const DefaultAccessProviders = {
AliyunAccessProvider,
DnspodAccessProvider,
TencentAccessProvider,
}
export default {
install () {
_.forEach(DefaultAccessProviders, item => {
accessProviderRegistry.install(item)
})
}
}

View File

@ -0,0 +1,34 @@
import _ from 'lodash-es'
export class AliyunAccessProvider{
static define () {
return {
name: 'aliyun',
label: '阿里云',
desc: '',
input: {
accessKeyId: {
type: String,
component: {
placeholder: 'accessKeyId',
rules: [{ required: true, message: '必填项' }]
},
required: true
},
accessKeySecret: {
type: String,
component: {
placeholder: 'accessKeySecret',
rules: [{ required: true, message: '必填项' }]
}
}
},
output: {
}
}
}
constructor () {
}
}

View File

@ -0,0 +1,30 @@
export class DnspodAccessProvider {
static define () {
return {
name: 'dnspod',
label: 'dnspod',
desc: '腾讯云的域名解析接口已迁移到dnspod',
input: {
id: {
type: String,
component: {
placeholder: 'dnspod接口账户id',
rules: [{ required: true, message: '该项必填' }]
}
},
token: {
type: String,
label: 'token',
component: {
placeholder: '开放接口token',
rules: [{ required: true, message: '该项必填' }]
}
}
}
}
}
constructor () {
}
}

View File

@ -0,0 +1,30 @@
export class TencentAccessProvider {
static define () {
return {
name: 'tencent',
label: '腾讯云',
input: {
secretId: {
type: String,
label:'secretId',
component: {
placeholder: 'secretId',
rules: [{ required: true, message: '该项必填' }]
}
},
secretKey: {
type: String,
label: 'secretKey',
component: {
placeholder: 'secretKey',
rules: [{ required: true, message: '该项必填' }]
}
}
}
}
}
constructor () {
}
}

View File

@ -0,0 +1,2 @@
import { Registry } from '../registry/registry.js'
export const accessProviderRegistry = new Registry()

View File

@ -1,8 +1,9 @@
import _ from 'lodash-es' import _ from 'lodash-es'
import logger from '../utils/util.log.js' import logger from '../utils/util.log.js'
export class AbstractDnsProvider { export class AbstractDnsProvider {
constructor () { constructor ({ accessProviders }) {
this.logger = logger this.logger = logger
this.accessProviders = accessProviders
} }
async createRecord ({ fullRecord, type, value }) { async createRecord ({ fullRecord, type, value }) {
@ -31,4 +32,11 @@ export class AbstractDnsProvider {
} }
return domain return domain
} }
getAccessProvider (accessProvider, accessProviders = this.accessProviders) {
if (typeof accessProvider === 'string' && accessProviders) {
accessProvider = accessProviders[accessProvider]
}
return accessProvider
}
} }

View File

@ -1,2 +1,3 @@
export { providerRegistry } from './provider-registry.js' import { Registry } from '../registry/registry.js'
export { AbstractDnsProvider } from './abstract-provider.js' export { AbstractDnsProvider } from './abstract-dns-provider.js'
export const dnsProviderRegistry = new Registry()

View File

@ -1,26 +0,0 @@
export class ProviderRegistry {
constructor () {
this.providers = {}
}
install (provider) {
if (provider == null) {
return
}
if (this.providers == null) {
this.providers = {}
}
const name = provider.name || (provider.define && provider.define.name)
this.providers[name] = provider
}
get (name) {
if (name) {
return this.providers[name]
}
throw new Error(`找不到授权提供者:${name}`)
}
}
export const providerRegistry = new ProviderRegistry()

View File

@ -1,4 +1,5 @@
export * from './dns-provider/index.js' export * from './dns-provider/index.js'
export * from './plugin/index.js' export * from './plugin/index.js'
export * from './access-provider/index.js'
export { Store } from './store/store.js' export { Store } from './store/store.js'
export { util } from './utils/index.js' export { util } from './utils/index.js'

View File

@ -3,8 +3,6 @@ import logger from '../utils/util.log.js'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import Sleep from '../utils/util.sleep.js' import Sleep from '../utils/util.sleep.js'
import { pluginRegistry } from './plugin-registry.js'
export class AbstractPlugin { export class AbstractPlugin {
constructor ({ accessProviders }) { constructor ({ accessProviders }) {
this.logger = logger this.logger = logger
@ -55,7 +53,7 @@ export class AbstractPlugin {
} }
/** /**
* 回退如有必要 * 回退用于单元测试
* @param options * @param options
*/ */
async rollback (options) { async rollback (options) {

View File

@ -1,2 +1,3 @@
export { pluginRegistry } from './plugin-registry.js' import { Registry } from '../registry/registry.js'
export { AbstractPlugin } from './abstract-plugin.js' export { AbstractPlugin } from './abstract-plugin.js'
export const pluginRegistry = new Registry()

View File

@ -1,27 +0,0 @@
export class PluginRegistry {
constructor () {
this.plugins = {}
}
install (plugin) {
if (plugin == null) {
return
}
if (this.plugins == null) {
this.plugins = {}
}
const name = plugin.name || (plugin.define && plugin.define.name)
this.plugins[name] = plugin
}
get (name) {
if (name) {
return this.plugins[name]
}
throw new Error(`找不到${name}插件`)
}
}
export const pluginRegistry = new PluginRegistry()

View File

@ -0,0 +1,34 @@
export class Registry {
constructor () {
this.collection = {}
}
install (target) {
if (target == null) {
return
}
if (this.collection == null) {
this.collection = {}
}
const className = target.name
this.register(className, target)
const defineName = target.define && target.define().name
this.register(defineName, target)
}
register (key, value) {
if (!key || value == null) {
return
}
this.collection[key] = value
}
get (name) {
if (name) {
return this.collection[name]
}
throw new Error(`${name} not found`)
}
}

View File

@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"@certd/acme-client": "^0.1.6", "@certd/acme-client": "^0.1.6",
"@certd/api": "^0.1.11", "@certd/api": "^0.1.11",
"@certd/providers": "^0.1.11", "@certd/dns-providers": "^0.1.11",
"dayjs": "^1.9.7", "dayjs": "^1.9.7",
"lodash-es": "^4.17.20", "lodash-es": "^4.17.20",
"node-forge": "^0.10.0" "node-forge": "^0.10.0"

View File

@ -1,4 +1,4 @@
import { util, Store, providerRegistry } from '@certd/api' import { util, Store, dnsProviderRegistry } from '@certd/api'
import { AcmeService } from './acme.js' import { AcmeService } from './acme.js'
import { FileStore } from './store/file-store.js' import { FileStore } from './store/file-store.js'
import { CertStore } from './store/cert-store.js' import { CertStore } from './store/cert-store.js'
@ -72,16 +72,14 @@ export class Certd {
} }
createDnsProvider (options) { createDnsProvider (options) {
const accessProviders = options.accessProviders return this.createProviderByType(options.cert.dnsProvider, options.accessProviders)
const providerOptions = accessProviders[options.cert.dnsProvider]
return this.createProviderByType(providerOptions.providerType, providerOptions)
} }
async writeCert (cert) { async writeCert (cert) {
const newPath = await this.certStore.writeCert(cert) const newPath = await this.certStore.writeCert(cert)
return { return {
realPath: this.certStore.store.getActualKey(newPath), realPath: this.certStore.store.getActualKey(newPath),
currentPath: this.certStore.store.getActualKey(this.certStore.currentRootPath) currentPath: this.certStore.store.getActualKey(this.certStore.currentMarkPath)
} }
} }
@ -122,12 +120,13 @@ export class Certd {
} }
} }
createProviderByType (type, options) { createProviderByType (props, accessProviders) {
const { type } = props
try { try {
const Provider = providerRegistry.get(type) const Provider = dnsProviderRegistry.get(type)
return new Provider(options) return new Provider({ accessProviders, props })
} catch (e) { } catch (e) {
throw new Error('暂不支持此dnsProvider,请先use该provider' + type, e) throw new Error('暂不支持此dnsProvider,请先注册该provider' + type, e)
} }
} }
} }

View File

@ -1,5 +1,6 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import crypto from 'crypto' import crypto from 'crypto'
// eslint-disable-next-line no-unused-vars
function md5 (content) { function md5 (content) {
return crypto.createHash('md5').update(content).digest('hex') return crypto.createHash('md5').update(content).digest('hex')
} }
@ -10,10 +11,11 @@ export class CertStore {
this.domains = domains this.domains = domains
this.domain = this.getMainDomain(this.domains) this.domain = this.getMainDomain(this.domains)
this.safetyDomain = this.getSafetyDomain(this.domain) this.safetyDomain = this.getSafetyDomain(this.domain)
this.domainDir = this.safetyDomain + '-' + md5(this.getDomainStr(this.domains)) // this.domainDir = this.safetyDomain + '-' + md5(this.getDomainStr(this.domains))
this.domainDir = this.safetyDomain
this.certsRootPath = this.store.buildKey(this.email, 'certs') this.certsRootPath = this.store.buildKey(this.email, 'certs')
this.currentRootPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current') this.currentMarkPath = this.store.buildKey(this.certsRootPath, this.domainDir, 'current.json')
} }
getMainDomain (domains) { getMainDomain (domains) {
@ -62,15 +64,19 @@ export class CertStore {
await this.store.set(priKey, this.formatCert(cert.key.toString())) await this.store.set(priKey, this.formatCert(cert.key.toString()))
await this.store.set(csrKey, cert.csr.toString()) await this.store.set(csrKey, cert.csr.toString())
await this.store.link(newDir, this.currentRootPath) await this.store.set(this.currentMarkPath, JSON.stringify({ latest: newDir }))
return newDir return newDir
} }
async readCert (dir) { async readCert (dir) {
if (dir == null) { if (dir == null) {
dir = this.currentRootPath dir = await this.getCurrentDir()
} }
if (dir == null) {
return
}
const crtKey = this.buildKey(dir, this.safetyDomain + '.crt') const crtKey = this.buildKey(dir, this.safetyDomain + '.crt')
const priKey = this.buildKey(dir, this.safetyDomain + '.key') const priKey = this.buildKey(dir, this.safetyDomain + '.key')
const csrKey = this.buildKey(dir, this.safetyDomain + '.csr') const csrKey = this.buildKey(dir, this.safetyDomain + '.csr')
@ -99,13 +105,23 @@ export class CertStore {
return domain.replace(/\*/g, '_') return domain.replace(/\*/g, '_')
} }
getCurrentFile (file) { async getCurrentDir () {
const key = this.buildKey(this.currentRootPath, file) const current = await this.store.get(this.currentMarkPath)
if (current == null) {
return null
}
return JSON.parse(current).latest
}
async getCurrentFile (file) {
const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.get(key) return this.store.get(key)
} }
setCurrentFile (file, value) { async setCurrentFile (file, value) {
const key = this.buildKey(this.currentRootPath, file) const currentDir = await this.getCurrentDir()
const key = this.buildKey(currentDir, file)
return this.store.set(key, value) return this.store.set(key, value)
} }
} }

View File

@ -66,9 +66,9 @@ describe('Certd', function () {
options.cert.email = 'xiaojunnuo@qq.com' options.cert.email = 'xiaojunnuo@qq.com'
options.cert.domains = ['*.docmirror.club'] options.cert.domains = ['*.docmirror.club']
const certd = new Certd(options) const certd = new Certd(options)
const currentRootPath = certd.certStore.currentRootPath const currentRootPath = certd.certStore.currentMarkPath
console.log('rootDir', currentRootPath) console.log('rootDir', currentRootPath)
expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club-\w+\\current/) expect(currentRootPath).match(/xiaojunnuo@qq.com\\certs\\_.docmirror.club\w*\\current.json/)
}) })
it('#writeAndReadCert', async function () { it('#writeAndReadCert', async function () {
const options = createOptions() const options = createOptions()
@ -83,6 +83,6 @@ describe('Certd', function () {
expect(cert.key).to.be.ok expect(cert.key).to.be.ok
expect(cert.detail).to.be.ok expect(cert.detail).to.be.ok
expect(cert.expires).to.be.ok expect(cert.expires).to.be.ok
console.log('expires:', cert.expires) console.log('cert:', JSON.stringify(cert))
}) })
}) })

View File

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

2814
packages/dns-providers/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "@certd/providers", "name": "@certd/dns-providers",
"version": "0.1.11", "version": "0.1.11",
"description": "", "description": "",
"main": "./src/index.js", "main": "./src/index.js",

View File

@ -0,0 +1,16 @@
import _ from 'lodash-es'
import { AliyunDnsProvider } from './providers/aliyun.js'
import { DnspodDnsProvider } from './providers/dnspod.js'
import { dnsProviderRegistry } from '@certd/api'
export const DefaultDnsProviders = {
AliyunDnsProvider,
DnspodDnsProvider
}
export default {
install () {
_.forEach(DefaultDnsProviders, item => {
dnsProviderRegistry.install(item)
})
}
}

View File

@ -8,21 +8,15 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
label: '阿里云', label: '阿里云',
desc: '', desc: '',
input: { input: {
accessKeyId: { accessProvider: {
type: String, label: 'Access提供者',
type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
component: { component: {
placeholder: 'accessKeyId', name: 'provider-selector',
rules: [{ required: true, message: '必填项' }] filter: 'aliyun'
}, },
required: true required: true
},
accessKeySecret: {
type: String,
component: {
placeholder: 'accessKeySecret',
rules: [{ required: true, message: '必填项' }]
}
} }
}, },
output: { output: {
@ -31,11 +25,13 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
} }
} }
constructor (dnsProviderConfig) { constructor (args) {
super() super(args)
const { props } = args
const accessProvider = this.getAccessProvider(props.accessProvider)
this.client = new Core({ this.client = new Core({
accessKeyId: dnsProviderConfig.accessKeyId, accessKeyId: accessProvider.accessKeyId,
accessKeySecret: dnsProviderConfig.accessKeySecret, accessKeySecret: accessProvider.accessKeySecret,
endpoint: 'https://alidns.aliyuncs.com', endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09' apiVersion: '2015-01-09'
}) })

View File

@ -8,31 +8,25 @@ export class DnspodDnsProvider extends AbstractDnsProvider {
label: 'dnspod(腾讯云)', label: 'dnspod(腾讯云)',
desc: '腾讯云的域名解析接口已迁移到dnspod', desc: '腾讯云的域名解析接口已迁移到dnspod',
input: { input: {
id: { accessProvider: {
type: String, label: 'Access提供者',
type: [String, Object],
desc: 'AccessProviders的key',
component: { component: {
placeholder: 'dnspod接口账户id', name: 'provider-selector',
rules: [{ required: true, message: '该项必填' }] filter: 'dnspod'
} },
}, required: true
token: {
type: String,
label: 'token',
component: {
placeholder: '开放接口token',
rules: [{ required: true, message: '该项必填' }]
}
} }
} }
} }
} }
constructor (dnsProviderConfig) { constructor (args) {
super() super(args)
if (!dnsProviderConfig.id || !dnsProviderConfig.token) { const { props } = args
throw new Error('请正确配置dnspod的 id 和 token') const accessProvider = this.getAccessProvider(props.accessProvider)
} this.loginToken = accessProvider.id + ',' + accessProvider.token
this.loginToken = dnsProviderConfig.id + ',' + dnsProviderConfig.token
} }
async doRequest (options) { async doRequest (options) {

View File

@ -1,5 +1,5 @@
import pkg from 'chai' import pkg from 'chai'
import AliyunDnsProvider from '../../src/dns-provider/aliyun.js' import AliyunDnsProvider from '../../src/providers/aliyun.js'
import { createOptions } from '../../../../test/options.js' import { createOptions } from '../../../../test/options.js'
const { expect } = pkg const { expect } = pkg
describe('AliyunDnsProvider', function () { describe('AliyunDnsProvider', function () {

View File

@ -1,5 +1,5 @@
import pkg from 'chai' import pkg from 'chai'
import DnspodDnsProvider from '../../src/dns-provider/dnspod.js' import DnspodDnsProvider from '../../src/providers/dnspod.js'
import { Certd } from '../../src/index.js' import { Certd } from '../../src/index.js'
import { createOptions } from '../../../../test/options.js' import { createOptions } from '../../../../test/options.js'
const { expect } = pkg const { expect } = pkg

View File

@ -13,6 +13,7 @@
"@certd/api": "^0.1.11", "@certd/api": "^0.1.11",
"@certd/certd": "^0.1.11", "@certd/certd": "^0.1.11",
"@certd/plugins": "^0.1.11", "@certd/plugins": "^0.1.11",
"@certd/dns-providers": "^0.1.11",
"dayjs": "^1.9.7", "dayjs": "^1.9.7",
"lodash-es": "^4.17.20" "lodash-es": "^4.17.20"
}, },

View File

@ -4,12 +4,13 @@ import _ from 'lodash-es'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Trace } from './trace.js' import { Trace } from './trace.js'
import DefaultPlugins from '@certd/plugins' import DefaultPlugins from '@certd/plugins'
import DefaultProviders from '@certd/providers' import DefaultDnsProviders from '@certd/dns-providers'
const logger = util.logger const logger = util.logger
// 安装默认插件和授权提供者 // 安装默认插件和授权提供者
DefaultPlugins.install() DefaultPlugins.install()
DefaultProviders.install() DefaultDnsProviders.install()
function createDefaultOptions () { function createDefaultOptions () {
return { return {

View File

@ -20,7 +20,7 @@ const define = {
} }
}, },
from: { from: {
value: 'upload', default: 'upload',
label: '证书来源', label: '证书来源',
component: { component: {
placeholder: '证书来源', placeholder: '证书来源',
@ -31,7 +31,7 @@ const define = {
{ value: 'cas', label: '从证书库', title: '需要uploadCertToAliyun作为前置任务' } { value: 'cas', label: '从证书库', title: '需要uploadCertToAliyun作为前置任务' }
] ]
}, },
desc: '如果选择cas类型,则需要以《上传证书到阿里云》作为前置任务' desc: '如果选择‘从证书库’类型,则需要以《上传证书到阿里云》作为前置任务'
}, },
// serverCertificateStatus: { // serverCertificateStatus: {

View File

@ -6,17 +6,22 @@ const define = {
label: '上传证书到阿里云', label: '上传证书到阿里云',
input: { input: {
name: { name: {
label: '证书名称' label: '证书名称',
desc: '证书上传后将以此参数作为名称前缀'
}, },
regionId: { regionId: {
label: '大区', label: '大区',
value: 'cn-hangzhou' default: 'cn-hangzhou'
}, },
accessProvider: { accessProvider: {
label: 'Access提供者', label: 'Access提供者',
type: [String, Object], type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象', desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=aliyun]' component: {
name: 'provider-selector',
filter: 'aliyun'
},
required: true
} }
}, },
output: { output: {

View File

@ -19,30 +19,27 @@ export class DeployCertToTencentCDN extends AbstractTencentPlugin {
required: true required: true
}, },
certName: { certName: {
label: '证书名称' label: '证书名称',
desc: '证书上传后将以此参数作为名称前缀'
}, },
certType: { certType: {
value: 'upload', default: 'upload',
label: '证书来源', label: '证书来源',
options: [ options: [
{ value: 'upload', label: '直接上传' }, { value: 'upload', label: '直接上传' },
{ value: 'cloud', label: '从证书库', desc: '需要uploadCertToTencent作为前置任务' } { value: 'cloud', label: '从证书库', desc: '需要uploadCertToTencent作为前置任务' }
], ],
desc: '如果选择‘从证书库’类型,则需要以《上传证书到腾讯云》作为前置任务',
required: true required: true
}, },
// serverCertificateStatus: {
// label: '启用https',
// options: [
// { value: 'on', label: '开启HTTPS并更新证书' },
// { value: 'auto', label: '若HTTPS开启则更新未开启不更新' }
// ],
// required:true
// },
accessProvider: { accessProvider: {
label: 'Access提供者', label: 'Access提供者',
type: [String, Object], type: [String, Object],
desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象', desc: 'AccessProviders的key 或 一个包含accessKeyId与accessKeySecret的对象',
options: 'accessProviders[type=aliyun]', component: {
name: 'provider-selector',
filter: 'tencent'
},
required: true required: true
} }
}, },

View File

@ -15,7 +15,7 @@ export class DeployCertToTencentCLB extends AbstractTencentPlugin {
input: { input: {
region: { region: {
label: '大区', label: '大区',
value: 'ap-guangzhou' default: 'ap-guangzhou'
}, },
domain: { domain: {
label: '域名', label: '域名',

View File

@ -16,7 +16,7 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
input: { input: {
region: { region: {
label: '大区', label: '大区',
value: 'ap-guangzhou' default: 'ap-guangzhou'
}, },
clusterId: { clusterId: {
label: '集群ID', label: '集群ID',
@ -25,7 +25,7 @@ export class DeployCertToTencentTKEIngress extends AbstractTencentPlugin {
}, },
namespace: { namespace: {
label: '集群的namespace', label: '集群的namespace',
value: 'default' default: 'default'
}, },
secreteName: { secreteName: {
type: [String, Array], type: [String, Array],

View File

@ -1,16 +0,0 @@
import _ from 'lodash-es'
import { AliyunDnsProvider } from './dns-provider/aliyun.js'
import { DnspodDnsProvider } from './dns-provider/dnspod.js'
import { providerRegistry } from '@certd/api'
export const DefaultProviders = {
AliyunDnsProvider,
DnspodDnsProvider
}
export default {
install () {
_.forEach(DefaultProviders, item => {
providerRegistry.install(item)
})
}
}

View File

@ -35,7 +35,10 @@ const defaultOptions = {
cert: { cert: {
domains: ['*.docmirror.cn'], domains: ['*.docmirror.cn'],
email: 'xiaojunnuo@qq.com', email: 'xiaojunnuo@qq.com',
dnsProvider: 'aliyun', dnsProvider: {
type:'aliyun',
accessProvider:'aliyun'
},
csrInfo: { csrInfo: {
country: 'CN', country: 'CN',
state: 'GuangDong', state: 'GuangDong',

View File

@ -1,5 +1,4 @@
import Koa from 'koa' import Koa from 'koa'
import views from 'koa-views'
import json from 'koa-json' import json from 'koa-json'
import onerror from 'koa-onerror' import onerror from 'koa-onerror'
import bodyparser from 'koa-bodyparser' import bodyparser from 'koa-bodyparser'
@ -7,6 +6,7 @@ import logger from 'koa-logger'
import Static from 'koa-static' import Static from 'koa-static'
import fs from 'fs' import fs from 'fs'
import _ from 'lodash-es' import _ from 'lodash-es'
const app = new Koa() const app = new Koa()
// error handler // error handler
@ -21,10 +21,6 @@ app.use(logger())
app.use(Static(new URL('public', import.meta.url).pathname)) app.use(Static(new URL('public', import.meta.url).pathname))
app.use(views(new URL('views', import.meta.url).pathname, {
extension: 'pug'
}))
// logger // logger
app.use(async (ctx, next) => { app.use(async (ctx, next) => {
const start = new Date() const start = new Date()
@ -33,15 +29,17 @@ app.use(async (ctx, next) => {
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
}) })
console.log('url', import.meta.url)
// routes // routes
const files = fs.readdirSync(new URL('controllers', import.meta.url).pathname) const files = fs.readdirSync(new URL('controllers/', import.meta.url))
// 过滤出.js文件: // 过滤出.js文件:
const jsFiles = files.filter((f) => { const jsFiles = files.filter((f) => {
return f.endsWith('.js') return f.endsWith('.js')
}) })
_.forEach(jsFiles, async item => { _.forEach(jsFiles, async item => {
let mapping = await import(new URL('controllers/' + item, import.meta.url).pathname) let mapping = await import(new URL('controllers/' + item, import.meta.url))
mapping = mapping.default mapping = mapping.default
app.use(mapping.routes(), mapping.allowedMethods()) app.use(mapping.routes(), mapping.allowedMethods())
}) })

View File

@ -0,0 +1,23 @@
import Router from 'koa-router'
import fs from 'fs'
import exportsService from '../service/exports-service.js'
// import executorPkg from '@certd/executor/package.json'
const router = Router()
router.prefix('/exports')
router.post('/toZip', async function (ctx, next) {
// const request = ctx.request
// const query = request.query
const body = ctx.request.body
// const req_queryString = request.queryString
const { zipPath, fileName } = await exportsService.exportsToZip(body, 'certd-run')
console.log('zipFile', zipPath)
ctx.set('Content-disposition', 'attachment;filename=' + fileName)
ctx.set('Content-Type', 'application/zip')
ctx.body = fs.createReadStream(zipPath)
//
// // ctx.body = Ret.success(zipPath)
})
export default router

View File

@ -1,6 +1,6 @@
import Router from 'koa-router' import Router from 'koa-router'
import { providerRegistry } from '@certd/api' import { providerRegistry } from '@certd/api'
import DefaultProviders from '@certd/providers' import DefaultProviders from '@certd/dns-providers'
import _ from 'lodash-es' import _ from 'lodash-es'
import { Ret } from '../models/Ret.js' import { Ret } from '../models/Ret.js'
const router = Router() const router = Router()

File diff suppressed because it is too large Load Diff

View File

@ -10,10 +10,13 @@
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"dependencies": { "dependencies": {
"@certd/plugins": "^0.1.11",
"@certd/providers": "^0.1.11",
"@certd/api": "^0.1.11", "@certd/api": "^0.1.11",
"@certd/executor": "^0.1.11",
"@certd/plugins": "^0.1.11",
"@certd/dns-providers": "^0.1.11",
"compressing": "^1.5.1",
"debug": "^4.1.1", "debug": "^4.1.1",
"fs-extra": "^9.1.0",
"koa": "^2.7.0", "koa": "^2.7.0",
"koa-bodyparser": "^4.2.1", "koa-bodyparser": "^4.2.1",
"koa-convert": "^1.2.0", "koa-convert": "^1.2.0",
@ -23,8 +26,7 @@
"koa-router": "^7.4.0", "koa-router": "^7.4.0",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"koa-views": "^6.2.0", "koa-views": "^6.2.0",
"lodash-es": "^4.17.20", "lodash-es": "^4.17.20"
"pug": "^2.0.3"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",

View File

@ -0,0 +1,39 @@
import os from 'os'
import fs from 'fs-extra'
import pathUtil from '../utils/util.path.js'
import cryptoRandomString from 'crypto-random-string'
import zipUtil from '../utils/util.zip.js'
import path from 'path'
export default {
async exportsToZip (options, dirName) {
const tempDir = os.tmpdir()
const targetDir = path.join(tempDir, 'certd-server', cryptoRandomString(10))
const projectName = dirName
const targetProjectDir = path.join(targetDir, projectName)
const templateDir = pathUtil.join('templates/' + projectName)
fs.copySync(templateDir, targetProjectDir)
// const packageFilePath = path.join(targetProjectDir, 'package.json')
const optionsFilePath = path.join(targetProjectDir, 'options.json')
fs.writeJsonSync(optionsFilePath, options)
const zipName = dirName + '.zip'
const outputFilePath = path.join(targetDir, zipName)
console.log('targetDir', targetDir)
console.log('projectName', projectName)
console.log('tempalteDir', templateDir)
console.log('targetProjectDir', targetProjectDir)
console.log('outputFilePath', outputFilePath)
await zipUtil.compress({ dir: targetProjectDir, output: outputFilePath })
return {
dir: targetDir,
fileName: zipName,
zipPath: outputFilePath
}
}
}

View File

@ -0,0 +1,4 @@
import { Executor } from '@certd/executor'
import options from './options.json'
const executor = new Executor()
executor.run(options)

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,19 @@
{
"name": "certd-run",
"version": "1.0.0",
"description": "certd run",
"main": "index.js",
"scripts": {
"certd": "node index.js"
},
"type": "module",
"repository": {
"type": "git",
"url": "https://github.com/certd/certd"
},
"author": "greper",
"license": "MIT",
"dependencies": {
"@certd/executor": "^0.1.11"
}
}

View File

@ -0,0 +1,6 @@
export default {
join (...dirs) {
const url = new URL('../' + dirs.join('/'), import.meta.url)
return url.href.replace(/^file:\/\/\//, '').replace(/^file:\/\//, '')
}
}

View File

@ -0,0 +1,8 @@
import compressing from 'compressing'
export default {
compress ({
dir, output
}) {
return compressing.zip.compressDir(dir, output)
}
}

View File

@ -1,6 +0,0 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

View File

@ -1,5 +0,0 @@
extends layout
block content
h1= title
p Welcome to #{title}

View File

@ -1,7 +0,0 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content

View File

@ -1153,9 +1153,9 @@
"tencentcloud-sdk-nodejs": "^4.0.44" "tencentcloud-sdk-nodejs": "^4.0.44"
} }
}, },
"@certd/providers": { "@certd/dns-providers": {
"version": "0.1.11", "version": "0.1.11",
"resolved": "https://registry.npmjs.org/@certd/providers/-/providers-0.1.11.tgz", "resolved": "https://registry.npmjs.org/@certd/dns-providers/-/providers-0.1.11.tgz",
"integrity": "sha512-km3WluZzNLNA0aeaJjQKochpx52CC5qrjN6Qql9FG+lJ+ceHl1P8oGU8oNUna13eRHcK54oe0vYQt8KkdFnrMQ==", "integrity": "sha512-km3WluZzNLNA0aeaJjQKochpx52CC5qrjN6Qql9FG+lJ+ceHl1P8oGU8oNUna13eRHcK54oe0vYQt8KkdFnrMQ==",
"requires": { "requires": {
"@alicloud/pop-core": "^1.7.10", "@alicloud/pop-core": "^1.7.10",
@ -3116,6 +3116,27 @@
"webpack-sources": "^1.4.3" "webpack-sources": "^1.4.3"
} }
}, },
"vue-loader-v15": {
"version": "npm:vue-loader@15.9.6",
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.6.tgz",
"integrity": "sha512-j0cqiLzwbeImIC6nVIby2o/ABAWhlppyL/m5oJ67R5MloP0hj/DtFgb0Zmq3J9CG7AJ+AXIvHVnJAPBvrLyuDg==",
"dev": true,
"requires": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"dependencies": {
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz",
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
"dev": true
}
}
},
"webpack": { "webpack": {
"version": "5.19.0", "version": "5.19.0",
"resolved": "https://registry.npm.taobao.org/webpack/download/webpack-5.19.0.tgz", "resolved": "https://registry.npm.taobao.org/webpack/download/webpack-5.19.0.tgz",
@ -6363,9 +6384,9 @@
"integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==" "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w=="
}, },
"dayjs": { "dayjs": {
"version": "1.10.3", "version": "1.10.4",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.3.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.4.tgz",
"integrity": "sha512-/2fdLN987N8Ki7Id8BUN2nhuiRyxTLumQnSQf9CNncFCyqFsSKb9TNhzRYcC8K8eJSJOKvbvkImo/MKKhNi4iw==" "integrity": "sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw=="
}, },
"debug": { "debug": {
"version": "4.3.1", "version": "4.3.1",
@ -9602,31 +9623,23 @@
} }
}, },
"internal-slot": { "internal-slot": {
"version": "1.0.2", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz",
"integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==",
"requires": { "requires": {
"es-abstract": "^1.17.0-next.1", "get-intrinsic": "^1.1.0",
"has": "^1.0.3", "has": "^1.0.3",
"side-channel": "^1.0.2" "side-channel": "^1.0.4"
}, },
"dependencies": { "dependencies": {
"es-abstract": { "get-intrinsic": {
"version": "1.17.7", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz",
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==",
"requires": { "requires": {
"es-to-primitive": "^1.2.1",
"function-bind": "^1.1.1", "function-bind": "^1.1.1",
"has": "^1.0.3", "has": "^1.0.3",
"has-symbols": "^1.0.1", "has-symbols": "^1.0.1"
"is-callable": "^1.2.2",
"is-regex": "^1.1.1",
"object-inspect": "^1.8.0",
"object-keys": "^1.1.1",
"object.assign": "^4.1.1",
"string.prototype.trimend": "^1.0.1",
"string.prototype.trimstart": "^1.0.1"
} }
} }
} }
@ -12333,6 +12346,11 @@
} }
} }
}, },
"object-hash": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz",
"integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ=="
},
"object-inspect": { "object-inspect": {
"version": "1.9.0", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz",
@ -12514,11 +12532,6 @@
"yallist": "^4.0.0" "yallist": "^4.0.0"
} }
}, },
"object-hash": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.1.1.tgz",
"integrity": "sha512-VOJmgmS+7wvXf8CjbQmimtCnEx3IAoLxI3fp2fbWehxrWBcAQFbk+vcwb6vzR0VZv/eNCJ/27j151ZTwqW/JeQ=="
},
"yallist": { "yallist": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
@ -14570,6 +14583,11 @@
"integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
"dev": true "dev": true
}, },
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
},
"prettier": { "prettier": {
"version": "1.19.1", "version": "1.19.1",
"resolved": "https://registry.npm.taobao.org/prettier/download/prettier-1.19.1.tgz?cache=0&sync_timestamp=1606521141305&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fprettier%2Fdownload%2Fprettier-1.19.1.tgz", "resolved": "https://registry.npm.taobao.org/prettier/download/prettier-1.19.1.tgz?cache=0&sync_timestamp=1606521141305&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fprettier%2Fdownload%2Fprettier-1.19.1.tgz",
@ -16460,9 +16478,9 @@
} }
}, },
"tencentcloud-sdk-nodejs": { "tencentcloud-sdk-nodejs": {
"version": "4.0.51", "version": "4.0.64",
"resolved": "https://registry.npmjs.org/tencentcloud-sdk-nodejs/-/tencentcloud-sdk-nodejs-4.0.51.tgz", "resolved": "https://registry.npmjs.org/tencentcloud-sdk-nodejs/-/tencentcloud-sdk-nodejs-4.0.64.tgz",
"integrity": "sha512-ZdayaDpby5RPR8m5/pUQOaEMkRek655Bp5c9ySgruqVhJskEwOQmzbSCGD3MFf7frV/n038vOSbXuvr0iRILOw==", "integrity": "sha512-Nh0SVRA+MEdLLMt4Uy8UgZD/CT+UDGZwca4sYFso0m/qO8RpCk6z9Jieq7Ltp5FAi3b2cV2GFgB6GrItoe+SUQ==",
"requires": { "requires": {
"babel-eslint": "^10.0.2", "babel-eslint": "^10.0.2",
"eslint-plugin-react": "^7.17.0", "eslint-plugin-react": "^7.17.0",
@ -17065,13 +17083,6 @@
"integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=",
"requires": { "requires": {
"prepend-http": "^2.0.0" "prepend-http": "^2.0.0"
},
"dependencies": {
"prepend-http": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz",
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc="
}
} }
}, },
"use": { "use": {
@ -17322,27 +17333,6 @@
} }
} }
}, },
"vue-loader-v15": {
"version": "npm:vue-loader@15.9.6",
"resolved": "https://registry.npm.taobao.org/vue-loader/download/vue-loader-15.9.6.tgz?cache=0&sync_timestamp=1608188009078&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fvue-loader%2Fdownload%2Fvue-loader-15.9.6.tgz",
"integrity": "sha1-9Lua4gw6g3CvPs8JuBJtOP/ba4s=",
"dev": true,
"requires": {
"@vue/component-compiler-utils": "^3.1.0",
"hash-sum": "^1.0.2",
"loader-utils": "^1.1.0",
"vue-hot-reload-api": "^2.3.0",
"vue-style-loader": "^4.1.0"
},
"dependencies": {
"hash-sum": {
"version": "1.0.2",
"resolved": "https://registry.npm.taobao.org/hash-sum/download/hash-sum-1.0.2.tgz",
"integrity": "sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=",
"dev": true
}
}
},
"vue-router": { "vue-router": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.0.3.tgz",

View File

@ -10,7 +10,7 @@
}, },
"dependencies": { "dependencies": {
"@certd/plugins": "^0.1.11", "@certd/plugins": "^0.1.11",
"@certd/providers": "^0.1.11", "@certd/dns-providers": "^0.1.11",
"ant-design-vue": "^2.0.0-rc.8", "ant-design-vue": "^2.0.0-rc.8",
"lodash-es": "^4.17.20", "lodash-es": "^4.17.20",
"vue-i18n": "^9.0.0-rc.2", "vue-i18n": "^9.0.0-rc.2",

View File

@ -0,0 +1,25 @@
import { request } from './service'
export default {
exportsToZip (options) {
return request({
url: '/exports/toZip',
data: { options },
method: 'post',
responseType: 'blob' // 重点在于配置responseType: 'blob'
}).then(res => {
console.log('res', res)
const filename = decodeURI(res.headers['content-disposition'].replace('attachment;filename=', '')) // 由后端设置下载文件名
const blob = new Blob([res.data], { type: 'application/zip' })
const a = document.createElement('a')
const url = window.URL.createObjectURL(blob)
a.href = url
a.download = filename
const body = document.getElementsByTagName('body')[0]
body.appendChild(a)
a.click()
body.removeChild(a)
window.URL.revokeObjectURL(url)
})
}
}

View File

@ -20,6 +20,10 @@ function createService () {
// 响应拦截 // 响应拦截
service.interceptors.response.use( service.interceptors.response.use(
response => { response => {
console.log('response.config', response.config)
if (response.config.responseType === 'blob') {
return response
}
// dataAxios 是 axios 返回数据中的 data // dataAxios 是 axios 返回数据中的 data
const dataAxios = response.data const dataAxios = response.data
// 这个状态码是和后端约定的 // 这个状态码是和后端约定的

View File

@ -76,6 +76,7 @@ function useTaskForm (context) {
const taskPluginDefineList = ref([]) const taskPluginDefineList = ref([])
const onCreated = async () => { const onCreated = async () => {
const plugins = await pluginsApi.list() const plugins = await pluginsApi.list()
console.log('plugins', plugins)
taskPluginDefineList.value = plugins taskPluginDefineList.value = plugins
} }
@ -113,7 +114,15 @@ function useTaskForm (context) {
message.warn('请先选择类型') message.warn('请先选择类型')
return return
} }
// taskinput
changeCurrentPlugin(currentTask.value) changeCurrentPlugin(currentTask.value)
for (const key in currentPlugin.value.input) {
const input = currentPlugin.value.input[key]
if (input.default != null) {
currentTask.value[key] = input.default
}
}
} }
const taskDrawerShow = () => { const taskDrawerShow = () => {
@ -245,6 +254,7 @@ export default {
font-size: 10px; font-size: 10px;
line-height: 20px; line-height: 20px;
height: 40px; height: 40px;
color: #7f7f7f
} }
} }
} }

View File

@ -134,6 +134,20 @@
</a-card> </a-card>
</div> </div>
</div> </div>
<div class="flow-group flow-export">
<h3 class="group-head">
导出
</h3>
<a-divider></a-divider>
<div class="export">
<div><a-button @click="exportsToZip"></a-button></div>
<br/>
<div> <a-button>仅导出配置</a-button></div>
</div>
</div>
</div> </div>
<cert-form ref="certFormRef" v-model:cert="options.cert" v-model:access-providers="options.accessProviders"></cert-form> <cert-form ref="certFormRef" v-model:cert="options.cert" v-model:access-providers="options.accessProviders"></cert-form>
@ -145,11 +159,12 @@
<script> <script>
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { reactive, ref, toRef, provide, readonly } from 'vue' import { reactive, ref, toRef, toRefs, provide, readonly } from 'vue'
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import CertForm from '@/views/detail/components/cert-form' import CertForm from '@/views/detail/components/cert-form'
import TaskForm from './components/task-form' import TaskForm from './components/task-form'
import exportsApi from '@/api/api.exports'
import _ from 'lodash-es' import _ from 'lodash-es'
function useDeploy (options) { function useDeploy (options) {
@ -187,6 +202,14 @@ function useProvideAccessProviders (options) {
}) })
} }
function useExports (options) {
return {
async exportsToZip () {
await exportsApi.exportsToZip(options)
}
}
}
export default { export default {
components: { CertForm, TaskForm }, components: { CertForm, TaskForm },
setup () { setup () {
@ -238,7 +261,8 @@ export default {
...useDeploy(options), ...useDeploy(options),
taskFormRef, taskFormRef,
taskAdd, taskAdd,
taskEdit taskEdit,
...useExports(options)
} }
} }
} }
@ -360,6 +384,10 @@ export default {
} }
} }
.flow-export{
max-width: 300px;
}
} }
} }