diff --git a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue index 707f118a4..8a42a66a9 100644 --- a/packages/ui/certd-client/src/components/plugins/common/remote-select.vue +++ b/packages/ui/certd-client/src/components/plugins/common/remote-select.vue @@ -138,6 +138,7 @@ const getOptions = async () => { onError(err: any) { hasError.value = true; message.value = `获取选项出错:${err.message}`; + optionsRef.value = []; }, showErrorNotify: false, } diff --git a/packages/ui/certd-server/src/plugins/index.ts b/packages/ui/certd-server/src/plugins/index.ts index 4fdadb6b1..651c61b27 100644 --- a/packages/ui/certd-server/src/plugins/index.ts +++ b/packages/ui/certd-server/src/plugins/index.ts @@ -41,4 +41,5 @@ export * from './plugin-xinnetconnet/index.js' export * from './plugin-oauth/index.js' export * from './plugin-cmcc/index.js' export * from './plugin-template/index.js' -export * from './plugin-ucloud/index.js' \ No newline at end of file +export * from './plugin-ucloud/index.js' +export * from './plugin-goedge/index.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-goedge/access.ts b/packages/ui/certd-server/src/plugins/plugin-goedge/access.ts new file mode 100644 index 000000000..747908709 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-goedge/access.ts @@ -0,0 +1,264 @@ +import {AccessInput, BaseAccess, IsAccess} from "@certd/pipeline"; +import {HttpRequestConfig} from "@certd/basic"; +import { CertInfo, CertReader } from "@certd/plugin-cert"; +import dayjs from "dayjs"; + + +/** + */ +@IsAccess({ + name: "goedge", + title: "GoEdge授权", + icon: "fa:leaf:#6C6BF6", + order: 100 +}) +export class GoEdgeAccess extends BaseAccess { + + @AccessInput({ + title: "系统地址", + component: { + name: "a-input", + vModel: "value" + }, + helper:"例如:http://yourdomain.com:8002, 需要在API节点配置中开启HTTP访问地址", + encrypt: false, + required: true + }) + endpoint!: string; + + @AccessInput({ + title: "用户类型", + component: { + name: "a-select", + vModel: "value", + options: [ + { + label: "用户", + value: "user" + }, + { + label: "管理员", + value: "admin" + } + ] + }, + encrypt: false, + required: true + }) + userType!: string; + + @AccessInput({ + title: "accessKeyId", + helper:`用户AccessKey: 在”平台用户-用户-详情-AccessKey” 或 商业版的“访问控制” 中创建。 +管理员AccessKey:在”系统用户-用户-详情-AccessKey” 中创建。`, + component: { + name: "a-input", + vModel: "value" + }, + encrypt: false, + required: true + }) + accessKeyId!: string; + + @AccessInput({ + title: "accessKey", + component: { + name: "a-input", + vModel: "value" + }, + encrypt: true, + required: true + }) + accessKey!: string; + + + + @AccessInput({ + title: "测试", + component: { + name: "api-test", + action: "TestRequest" + }, + helper: "点击测试接口是否正常" + }) + testRequest = true; + + accessToken: {expiresAt:number,token:string} + + async onTestRequest() { + await this.getCertList({pageSize:1}); + return "ok" + } + + /** + * + * @param req "id": 600, + "isOn": true, + "name": "124.220.225.222", + "description": "", + "certData": null, + "keyData": null, + "serverName": "", + "isCA": false, + "isACME": false, + "timeBeginAt": 1763856000, + "timeEndAt": 1771718399, + "dnsNames": [ + "124.220.225.222" //domain + ], + "commonNames": [ + "ZeroSSL ECC Domain Secure Site CA", + "USERTrust ECC Certification Authority" + ], + "ocsp": null, + "ocspExpiresAt": 0, + "ocspError": "" + * @returns + */ + async getCertList(req:{pageNo?:number,pageSize?:number,query?:string,onlyUser?:boolean,userId?:number}){ + const pageNo = req.pageNo ?? 1; + const pageSize = req.pageSize ?? 20; + const body:any = { + keyword: req.query??"", + offset: (pageNo-1)*pageSize, + size: pageSize, + } + if (req.onlyUser){ + body["onlyUser"] = true; + } + if (req.userId){ + body["userId"] = req.userId; + } + + const countRes = await this.doRequest({ + url: `/SSLCertService/countSSLCerts`, + method: "POST", + data:body + }); + const total = countRes.count || 9999; + + const res = await this.doRequest({ + url: `/SSLCertService/listSSLCerts`, + method: "POST", + data:body + }); + // this.ctx.logger.info("getCertList",JSON.stringify(res)); + const sslCertsJSON = this.ctx.utils.hash.base64Decode(res.sslCertsJSON) || "[]"; + const sslCerts = JSON.parse(sslCertsJSON) as CertInfo[]; + return { + total: total, + list: sslCerts || [], + pageNo: pageNo, + pageSize: pageSize + } + } + + async doCertReplace(req:{certId:number,cert:CertInfo}){ + + const res = await this.doRequest({ + url: `/SSLCertService/findEnabledSSLCertConfig`, + method: "POST", + data: { + sslCertId: req.certId, + } + }); + const sslCertJSON = this.ctx.utils.hash.base64Decode(res.sslCertJSON) || "{}"; + const sslCert = JSON.parse(sslCertJSON) ; + + const certReader = new CertReader(req.cert); + const dnsNames = certReader.getAllDomains() + + // /product/sslcenter/{id} + return await this.doRequest({ + url: `/SSLCertService/updateSSLCert`, + method: "POST", + data: { + sslCertId: req.certId, + certData: this.ctx.utils.hash.base64(req.cert.crt), + keyData: this.ctx.utils.hash.base64(req.cert.key), + isOn: sslCert.isOn, + name: sslCert.name || certReader.buildCertName(), + description: sslCert.description || "upload by certd", + serverName: sslCert.serverName, + timeBeginAt: certReader.detail.notBefore.getTime()/1000, + timeEndAt: certReader.detail.notAfter.getTime()/1000, + dnsNames: dnsNames, + /** + * // 是否启用 + bool isOn; + + // 名称 + string name; + + // 描述(备注) + string description; + string serverName; + bool isCA; + bytes certData; + bytes keyData; + int64 timeBeginAt; + int64 timeEndAt; + []string dnsNames; + []string commonNames; + */ + } + }); + + } + + + async getToken(){ + // /APIAccessTokenService/getAPIAccessToken + if (this.accessToken && this.accessToken.expiresAt >dayjs().unix()){ + return this.accessToken; + } + + const res = await this.doRequest({ + url: "/APIAccessTokenService/getAPIAccessToken", + method: "POST", + data: { + type: this.userType, + "accessKeyId": this.accessKeyId, + "accessKey": this.accessKey, + } + }); + this.accessToken = res; + return res; + } + + async doRequest(req:HttpRequestConfig){ + + const headers: Record = {} + if(!req.url.endsWith("/getAPIAccessToken")){ + if (!this.accessToken || this.accessToken.expiresAt < dayjs().unix()){ + await this.getToken(); + } + headers["X-Edge-Access-Token"] = this.accessToken.token; + } + let endpoint = this.endpoint; + if (endpoint.endsWith("/")){ + endpoint = endpoint.slice(0,-1); + } + const res = await this.ctx.http.request({ + url: req.url, + baseURL: endpoint, + method: req.method|| "POST", + data: req.data, + params: req.params, + headers:{ + ...headers, + ...req.headers + }, + // httpProxy: this.httpProxy||undefined, + }); + + if (res.code === 200) { + return res.data; + } + throw new Error(res.message || res); + } +} + + + +new GoEdgeAccess(); diff --git a/packages/ui/certd-server/src/plugins/plugin-goedge/index.ts b/packages/ui/certd-server/src/plugins/plugin-goedge/index.ts new file mode 100644 index 000000000..02dc3945d --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-goedge/index.ts @@ -0,0 +1,2 @@ +export * from "./plugins/index.js"; +export * from "./access.js"; diff --git a/packages/ui/certd-server/src/plugins/plugin-goedge/plugins/index.ts b/packages/ui/certd-server/src/plugins/plugin-goedge/plugins/index.ts new file mode 100644 index 000000000..3ce4f30c7 --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-goedge/plugins/index.ts @@ -0,0 +1 @@ +export * from './plugin-refresh-cert.js' diff --git a/packages/ui/certd-server/src/plugins/plugin-goedge/plugins/plugin-refresh-cert.ts b/packages/ui/certd-server/src/plugins/plugin-goedge/plugins/plugin-refresh-cert.ts new file mode 100644 index 000000000..2ae7f147d --- /dev/null +++ b/packages/ui/certd-server/src/plugins/plugin-goedge/plugins/plugin-refresh-cert.ts @@ -0,0 +1,131 @@ +import { AbstractTaskPlugin, IsTaskPlugin, PageSearch, pluginGroups, RunStrategy, TaskInput } from "@certd/pipeline"; +import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert"; +import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from "@certd/plugin-lib"; +import { GoEdgeAccess } from "../access.js"; + +@IsTaskPlugin({ + //命名规范,插件类型+功能(就是目录plugin-demo中的demo),大写字母开头,驼峰命名 + name: "GoEdgeRefreshCert", + title: "GoEdge-更新证书", + desc: "GoEdge", + icon: "fa:leaf:#6C6BF6", + //插件分组 + group: pluginGroups.cdn.key, + needPlus: false, + default: { + //默认值配置照抄即可 + strategy: { + runStrategy: RunStrategy.SkipWhenSucceed + } + } +}) +//类名规范,跟上面插件名称(name)一致 +export class GoEdgeRefreshCert extends AbstractTaskPlugin { + //证书选择,此项必须要有 + @TaskInput({ + title: "域名证书", + helper: "请选择前置任务输出的域名证书", + component: { + name: "output-selector", + from: [...CertApplyPluginNames] + } + // required: true, // 必填 + }) + cert!: CertInfo; + + @TaskInput(createCertDomainGetterInputDefine({ props: { required: false } })) + certDomains!: string[]; + + + //授权选择框 + @TaskInput({ + title: "GoEdge授权", + component: { + name: "access-selector", + type: "goedge" //固定授权类型 + }, + required: true //必填 + }) + accessId!: string; + // + + + @TaskInput({ + title: "用户id", + component: { + name: "a-input-number", + vModel: "value" + }, + helper:"用于查询用户证书,点击用户详情->浏览器地址中userId值,如:/users/user?userId=1\n如果为空,则查询管理员证书", + required: false //必填 + }) + userId!: number; + + @TaskInput( + createRemoteSelectInputDefine({ + title: "证书Id", + helper: "要更新的GoEdge证书id", + pager:true, + search:true, + action: GoEdgeRefreshCert.prototype.onGetCertList.name + }) + ) + certList!: number[]; + + //插件实例化时执行的方法 + async onInstance() { + } + + //插件执行方法 + async execute(): Promise { + const access = await this.getAccess(this.accessId); + + for (const item of this.certList) { + this.logger.info(`----------- 开始更新证书:${item}`); + await access.doCertReplace({ + certId: item, + cert: this.cert + }); + this.logger.info(`----------- 更新证书${item}成功`); + } + + this.logger.info("部署完成"); + } + + async onGetCertList(req: PageSearch = {}) { + const access = await this.getAccess(this.accessId); + + const pageNo = req.pageNo ?? 1; + const pageSize = req.pageSize ?? 100; + const res = await access.getCertList({ + pageNo, + pageSize, + query: req.searchKey, + userId: this.userId, + onlyUser: this.userId !== undefined + }); + const total = res.total; + const list = res.list; + if (!list || list.length === 0) { + throw new Error("没有找到证书,请先在控制台上传一次证书且关联站点"); + } + + const options = list.map((item: any) => { + return { + label: `${item.name}<${item.id}>`, + value: item.id, + domain: item.dnsNames || [], + title: item.dnsNames?.join(",") || "" + }; + }); + return { + list: this.ctx.utils.options.buildGroupOptions(options, this.certDomains), + total: total, + pageNo: pageNo, + pageSize: pageSize + }; + } +} + +//实例化一下,注册插件 +new GoEdgeRefreshCert(); diff --git a/packages/ui/certd-server/src/plugins/plugin-rainyun/access.ts b/packages/ui/certd-server/src/plugins/plugin-rainyun/access.ts index 54aade68c..7d9d1293f 100644 --- a/packages/ui/certd-server/src/plugins/plugin-rainyun/access.ts +++ b/packages/ui/certd-server/src/plugins/plugin-rainyun/access.ts @@ -19,10 +19,8 @@ export class RainyunAccess extends BaseAccess { title: "ApiKey", component: { placeholder: "api-key", - component: { - name: "a-input", - vModel: "value" - } + name: "a-input", + vModel: "value" }, helper:"https://app.rainyun.com/account/settings/api-key", encrypt: true,