mirror of https://github.com/certd/certd
Merge remote-tracking branch 'origin/v2-dev' into v2-dev
commit
79f8e5bf47
|
@ -87,3 +87,15 @@ jobs:
|
|||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||
greper/certd:armv7
|
||||
greper/certd:${{steps.get_certd_version.outputs.result}}-armv7
|
||||
|
||||
- name: Build agent
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
context: ./packages/ui/agent/
|
||||
tags: |
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:latest
|
||||
registry.cn-shenzhen.aliyuncs.com/handsfree/certd-agent:${{steps.get_certd_version.outputs.result}}
|
||||
greper/certd-agent:latest
|
||||
greper/certd-agent:${{steps.get_certd_version.outputs.result}}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
## 1.本地调试运行
|
||||
|
||||
安装依赖包:
|
||||
### 克隆代码
|
||||
```shell
|
||||
|
||||
# 克隆代码
|
||||
|
@ -11,22 +11,38 @@ git clone https://github.com/certd/certd
|
|||
|
||||
#进入项目目录
|
||||
cd certd
|
||||
```
|
||||
|
||||
### 修改pnpm-workspace.yaml文件
|
||||
重要:否则无法正确加载专业版的access和plugin
|
||||
```yaml
|
||||
# pnpm-workspace.yaml
|
||||
packages:
|
||||
- 'packages/**' # <--------------注释掉这一行,PR时不要提交此修改
|
||||
- 'packages/ui/**'
|
||||
```
|
||||
|
||||
### 安装依赖和初始化:
|
||||
```shell
|
||||
# 安装pnpm,如果提示npm命令不存在,就需要先安装nodejs
|
||||
npm install -g pnpm@8.15.7 --registry=https://registry.npmmirror.com
|
||||
|
||||
# 使用国内镜像源,如果有代理,就不需要
|
||||
pnpm config set registry https://registry.npmmirror.com
|
||||
# 安装依赖
|
||||
npm install -g pnpm@8.15.7
|
||||
pnpm install
|
||||
|
||||
# 初始化构建
|
||||
npm run init
|
||||
```
|
||||
|
||||
启动 server:
|
||||
### 启动 server:
|
||||
```shell
|
||||
cd packages/ui/certd-server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
启动 client:
|
||||
### 启动 client:
|
||||
```shell
|
||||
cd packages/ui/certd-client
|
||||
npm run dev
|
||||
|
@ -48,7 +64,7 @@ npm run dev
|
|||
这样用户就可以在`certd`后台中创建这种授权凭证了
|
||||
|
||||
### 3. dns-provider
|
||||
如果域名是这个平台进行解析的,那么你需要实现dns-provider
|
||||
如果域名是这个平台进行解析的,那么你需要实现dns-provider,(申请证书需要)
|
||||
参考`plugin-cloudflare/dns-provider.ts` 修改为你要做的平台的`dns-provider`
|
||||
|
||||
### 4. plugin-deploy
|
||||
|
@ -66,7 +82,7 @@ export * from './plugins/plugin-deploy-to-xx'
|
|||
在`./src/plugins/index.ts`中增加`import`
|
||||
|
||||
```ts
|
||||
export * from "./plugin-cloudflare"
|
||||
export * from "./plugin-cloudflare.js"
|
||||
```
|
||||
|
||||
## 重启服务进行调试
|
||||
|
|
|
@ -7,16 +7,18 @@ https://console.cloud.google.com/apis/library/publicca.googleapis.com
|
|||
|
||||
打开该链接后点击“启用”,随后等待右侧出现“API已启用”则可以关闭该页。
|
||||
|
||||
## 2、 申请Key
|
||||
随后打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。
|
||||
## 2、 获取授权
|
||||
以下两种方式任选其一
|
||||
### 2.1 直接获取EAB 【推荐】
|
||||
|
||||
|
||||
1. 打开“Google Cloud Shell”(在右上角点击激活CloudShell图标)。
|
||||
等待分配完成后在 Shell 窗口内输入如下命令:
|
||||
|
||||
```shell
|
||||
gcloud beta publicca external-account-keys create
|
||||
```
|
||||
此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。
|
||||
|
||||
2. 此时会弹出“为 Cloud Shell 提供授权”,点击授权即可。
|
||||
执行完成后会返回类似如下输出;注意不要在没有收到 Google 的邮件时执行该命令,会返回命令不存在。
|
||||
|
||||
```shell
|
||||
|
@ -24,14 +26,31 @@ Created an external account key
|
|||
[b64MacKey: xxxxxxxxxxxxxxxx
|
||||
keyId: xxxxxxxxxxxxx]
|
||||
```
|
||||
记录以上信息备用(注意keyId是不带中括号的)
|
||||
|
||||
3. 到Certd中,创建一条EAB授权记录,填写keyId(=kid) 和 b64MacKey 信息
|
||||
注意:keyId没有`]`结尾,不要把`]`也复制了
|
||||
|
||||
注意:EAB授权使用过一次之后,会绑定邮箱,后续再次使用时,要使用相同的邮箱
|
||||
否则会报错 `Unknown external account binding (EAB) key. This may be due to the EAB key expiring which occurs 7 days after creation`
|
||||
|
||||
### 2.2 通过服务账号获取EAB
|
||||
|
||||
此方式可以自动EAB,需要配置代理
|
||||
|
||||
1. 创建服务账号
|
||||
https://console.cloud.google.com/projectselector2/iam-admin/serviceaccounts/create?walkthrough_id=iam--create-service-account&hl=zh-cn#step_index=1
|
||||
|
||||
2. 选择一个项目,进入创建服务账号页面
|
||||
3. 给服务账号起一个名字,点击`创建并继续`
|
||||
4. 向此服务账号授予对项目的访问权限: `选择角色`->`基本`->`Owner`
|
||||
5. 点击完成
|
||||
6. 点击服务账号,进入服务账号详情页面
|
||||
7. 点击`添加密钥`->`创建新密钥`->`JSON`,下载密钥文件
|
||||
8. 将json文件内容粘贴到 certd中 Google服务授权输入框中
|
||||
|
||||
|
||||
## 3、 创建证书流水线
|
||||
选择证书提供商为google, 开启使用代理
|
||||
选择证书提供商为google, 选择EAB授权 或 服务账号授权
|
||||
|
||||
## 4、 将key信息作为EAB授权信息
|
||||
google证书需要EAB授权, 使用第二步中的 keyId 和 b64MacKey 信息创建一条EAB授权记录
|
||||
注意:keyId没有`]`结尾,不要把`]`也复制了
|
||||
## 5、 其他就跟正常申请证书一样了
|
||||
## 4、 其他就跟正常申请证书一样了
|
||||
|
||||
|
|
|
@ -54,3 +54,11 @@ export class SysSiteInfo extends BaseSettings {
|
|||
logo?: string;
|
||||
loginLogo?: string;
|
||||
}
|
||||
|
||||
export class SysSiteEnv {
|
||||
agent?: {
|
||||
enabled?: boolean;
|
||||
contactText?: string;
|
||||
contactLink?: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,9 +14,10 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/basic": "^1.25.9",
|
||||
"@certd/acme-client": "^1.25.9",
|
||||
"@certd/basic": "^1.25.9",
|
||||
"@certd/pipeline": "^1.25.9",
|
||||
"@google-cloud/publicca": "^1.3.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"jszip": "^3.10.1",
|
||||
"node-forge": "^0.10.0",
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
name: "google",
|
||||
title: "google cloud",
|
||||
desc: "谷歌云授权",
|
||||
})
|
||||
export class GoogleAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "密钥类型",
|
||||
value: "serviceAccount",
|
||||
component: {
|
||||
placeholder: "密钥类型",
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
options: [
|
||||
{ value: "serviceAccount", label: "服务账号密钥" },
|
||||
{ value: "apiKey", label: "ApiKey,暂不可用", disabled: true },
|
||||
],
|
||||
},
|
||||
helper: "密钥类型",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
})
|
||||
type = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "项目ID",
|
||||
component: {
|
||||
placeholder: "ProjectId",
|
||||
},
|
||||
helper: "ProjectId",
|
||||
required: true,
|
||||
encrypt: false,
|
||||
mergeScript: `
|
||||
return {
|
||||
show:ctx.compute(({form})=>{
|
||||
return form.access.type === 'apiKey'
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
projectId = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "ApiKey",
|
||||
component: {
|
||||
placeholder: "ApiKey",
|
||||
},
|
||||
helper: "不要选,目前没有用",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show:ctx.compute(({form})=>{
|
||||
return form.access.type === 'apiKey'
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
apiKey = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "服务账号密钥",
|
||||
component: {
|
||||
placeholder: "serviceAccountSecret",
|
||||
name: "a-textarea",
|
||||
vModel: "value",
|
||||
rows: 4,
|
||||
},
|
||||
helper:
|
||||
"[如何创建服务账号](https://cloud.google.com/iam/docs/service-accounts-create?hl=zh-CN) \n[获取密钥](https://console.cloud.google.com/iam-admin/serviceaccounts?hl=zh-cn),点击详情,点击创建密钥,将下载json文件,把内容填在此处",
|
||||
required: true,
|
||||
encrypt: true,
|
||||
mergeScript: `
|
||||
return {
|
||||
show:ctx.compute(({form})=>{
|
||||
return form.access.type === 'serviceAccount'
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
serviceAccountSecret = "";
|
||||
|
||||
@AccessInput({
|
||||
title: "https代理",
|
||||
component: {
|
||||
placeholder: "http://127.0.0.1:10811",
|
||||
},
|
||||
helper: "Google的请求需要走代理,如果不配置,则会使用环境变量中的全局HTTPS_PROXY配置\n或者服务器本身在海外,则不需要配置",
|
||||
required: false,
|
||||
encrypt: false,
|
||||
})
|
||||
httpsProxy = "";
|
||||
}
|
||||
|
||||
new GoogleAccess();
|
|
@ -1 +1,2 @@
|
|||
export * from "./eab-access.js";
|
||||
export * from "./google-access.js";
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { EabAccess, GoogleAccess } from "../access/index.js";
|
||||
import { ILogger } from "@certd/basic";
|
||||
|
||||
export class GoogleClient {
|
||||
access: GoogleAccess;
|
||||
logger: ILogger;
|
||||
constructor(opts: { logger: ILogger; access: GoogleAccess }) {
|
||||
this.access = opts.access;
|
||||
this.logger = opts.logger;
|
||||
}
|
||||
async getEab() {
|
||||
// https://cloud.google.com/docs/authentication/api-keys-use#using-with-client-libs
|
||||
const { v1 } = await import("@google-cloud/publicca");
|
||||
// process.env.HTTPS_PROXY = "http://127.0.0.1:10811";
|
||||
const access = this.access;
|
||||
if (!access.serviceAccountSecret) {
|
||||
throw new Error("服务账号密钥 不能为空");
|
||||
}
|
||||
const credentials = JSON.parse(access.serviceAccountSecret);
|
||||
|
||||
const client = new v1.PublicCertificateAuthorityServiceClient({ credentials });
|
||||
const parent = `projects/${credentials.project_id}/locations/global`;
|
||||
const externalAccountKey = {};
|
||||
const request = {
|
||||
parent,
|
||||
externalAccountKey,
|
||||
};
|
||||
|
||||
let envHttpsProxy = "";
|
||||
try {
|
||||
if (this.access.httpsProxy) {
|
||||
//设置临时使用代理
|
||||
envHttpsProxy = process.env.HTTPS_PROXY;
|
||||
process.env.HTTPS_PROXY = this.access.httpsProxy;
|
||||
}
|
||||
this.logger.info("开始获取google eab授权");
|
||||
const response = await client.createExternalAccountKey(request);
|
||||
const { keyId, b64MacKey } = response[0];
|
||||
const eabAccess = new EabAccess();
|
||||
eabAccess.kid = keyId;
|
||||
eabAccess.hmacKey = b64MacKey.toString();
|
||||
this.logger.info(`google eab授权获取成功,kid: ${eabAccess.kid}`);
|
||||
return eabAccess;
|
||||
} finally {
|
||||
if (envHttpsProxy) {
|
||||
process.env.HTTPS_PROXY = envHttpsProxy;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// const access = new GoogleAccess();
|
||||
// access.projectId = "hip-light-432411-d4";
|
||||
// access.serviceAccountSecret = `
|
||||
//
|
||||
//
|
||||
// `;
|
||||
// // process.env.HTTPS_PROXY = "http://127.0.0.1:10811";
|
||||
// const client = new GoogleClient(access);
|
||||
// client.getEab().catch((e) => {
|
||||
// console.error(e);
|
||||
// });
|
|
@ -40,6 +40,7 @@ type AcmeServiceOptions = {
|
|||
eab?: ClientExternalAccountBindingOptions;
|
||||
skipLocalVerify?: boolean;
|
||||
useMappingProxy?: boolean;
|
||||
reverseProxy?: string;
|
||||
privateKeyType?: PrivateKeyType;
|
||||
signal?: AbortSignal;
|
||||
};
|
||||
|
@ -91,8 +92,8 @@ export class AcmeService {
|
|||
const urlMapping: UrlMapping = {
|
||||
enabled: false,
|
||||
mappings: {
|
||||
"acme-v02.api.letsencrypt.org": "letsencrypt.proxy.handsfree.work",
|
||||
"dv.acme-v02.api.pki.goog": "google.proxy.handsfree.work",
|
||||
"acme-v02.api.letsencrypt.org": this.options.reverseProxy || "letsencrypt.proxy.handsfree.work",
|
||||
"dv.acme-v02.api.pki.goog": this.options.reverseProxy || "google.proxy.handsfree.work",
|
||||
},
|
||||
};
|
||||
const conf = await this.getAccountConfig(email, urlMapping);
|
||||
|
|
|
@ -5,6 +5,7 @@ import _ from "lodash-es";
|
|||
import { createDnsProvider, DnsProviderContext, IDnsProvider } from "../../dns-provider/index.js";
|
||||
import { CertReader } from "./cert-reader.js";
|
||||
import { CertApplyBasePlugin } from "./base.js";
|
||||
import { GoogleClient } from "../../libs/google.js";
|
||||
|
||||
export type { CertInfo };
|
||||
export * from "./cert-reader.js";
|
||||
|
@ -144,9 +145,9 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||
type: "eab",
|
||||
},
|
||||
maybeNeed: true,
|
||||
required: true,
|
||||
required: false,
|
||||
helper:
|
||||
"需要提供EAB授权\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials' \n Google:请查看[google获取eab帮助文档](https://github.com/certd/certd/blob/v2/doc/google/google.md)",
|
||||
"需要提供EAB授权\nZeroSSL:请前往[zerossl开发者中心](https://app.zerossl.com/developer),生成 'EAB Credentials'\n Google:请查看[google获取eab帮助文档](https://gitee.com/certd/certd/blob/v2/doc/google/google.md),用过一次后会绑定邮箱,后续复用EAB要用同一个邮箱",
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
|
@ -157,6 +158,26 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||
})
|
||||
eabAccessId!: number;
|
||||
|
||||
@TaskInput({
|
||||
title: "服务账号授权",
|
||||
component: {
|
||||
name: "access-selector",
|
||||
type: "google",
|
||||
},
|
||||
maybeNeed: true,
|
||||
required: false,
|
||||
helper:
|
||||
"google服务账号授权与EAB授权选填其中一个,[服务账号授权获取方法](https://gitee.com/certd/certd/blob/v2/doc/google/google.md)\n服务账号授权需要配置代理或者服务器本身在海外",
|
||||
mergeScript: `
|
||||
return {
|
||||
show: ctx.compute(({form})=>{
|
||||
return form.sslProvider === 'google'
|
||||
})
|
||||
}
|
||||
`,
|
||||
})
|
||||
googleAccessId!: number;
|
||||
|
||||
@TaskInput({
|
||||
title: "加密算法",
|
||||
value: "rsa_2048",
|
||||
|
@ -190,6 +211,15 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||
})
|
||||
useProxy = false;
|
||||
|
||||
@TaskInput({
|
||||
title: "自定义反代地址",
|
||||
component: {
|
||||
placeholder: "google.yourproxy.com",
|
||||
},
|
||||
helper: "填写你的自定义反代地址,不要带http://\nletsencrypt反代目标:acme-v02.api.letsencrypt.org\ngoogle反代目标:dv.acme-v02.api.pki.goog",
|
||||
})
|
||||
reverseProxy = "";
|
||||
|
||||
@TaskInput({
|
||||
title: "跳过本地校验DNS",
|
||||
value: false,
|
||||
|
@ -205,9 +235,32 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||
|
||||
async onInit() {
|
||||
let eab: any = null;
|
||||
if (this.eabAccessId) {
|
||||
eab = await this.ctx.accessService.getById(this.eabAccessId);
|
||||
|
||||
if (this.sslProvider === "google") {
|
||||
if (this.googleAccessId) {
|
||||
this.logger.info("您正在使用google服务账号授权");
|
||||
const googleAccess = await this.ctx.accessService.getById(this.googleAccessId);
|
||||
const googleClient = new GoogleClient({
|
||||
access: googleAccess,
|
||||
logger: this.logger,
|
||||
});
|
||||
eab = await googleClient.getEab();
|
||||
} else if (this.eabAccessId) {
|
||||
this.logger.info("您正在使用google EAB授权");
|
||||
eab = await this.ctx.accessService.getById(this.eabAccessId);
|
||||
} else {
|
||||
this.logger.error("google需要配置EAB授权或服务账号授权");
|
||||
return;
|
||||
}
|
||||
} else if (this.sslProvider === "zerossl") {
|
||||
if (this.eabAccessId) {
|
||||
eab = await this.ctx.accessService.getById(this.eabAccessId);
|
||||
} else {
|
||||
this.logger.error("zerossl需要配置EAB授权");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.acme = new AcmeService({
|
||||
userContext: this.userContext,
|
||||
logger: this.logger,
|
||||
|
@ -215,6 +268,7 @@ export class CertApplyPlugin extends CertApplyBasePlugin {
|
|||
eab,
|
||||
skipLocalVerify: this.skipLocalVerify,
|
||||
useMappingProxy: this.useProxy,
|
||||
reverseProxy: this.reverseProxy,
|
||||
privateKeyType: this.privateKeyType,
|
||||
// cnameProxyService: this.ctx.cnameProxyService,
|
||||
// dnsProviderCreator: this.createDnsProvider.bind(this),
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
FROM greper/certd:latest
|
||||
ENV certd_agent_enabled=true
|
||||
CMD ["npm", "run","start"]
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { request } from "../service";
|
||||
import { SiteEnv, SiteInfo } from "/@/store/modules/settings";
|
||||
|
||||
export type SysPublicSetting = {
|
||||
registerEnabled: boolean;
|
||||
|
@ -24,14 +25,20 @@ export async function getInstallInfo(): Promise<SysInstallInfo> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function getSiteInfo(): Promise<SysInstallInfo> {
|
||||
export async function getSiteInfo(): Promise<SiteInfo> {
|
||||
return await request({
|
||||
url: "/basic/settings/siteInfo",
|
||||
method: "get"
|
||||
});
|
||||
}
|
||||
export async function getSiteEnv(): Promise<SiteEnv> {
|
||||
return await request({
|
||||
url: "/basic/settings/siteEnv",
|
||||
method: "get"
|
||||
});
|
||||
}
|
||||
|
||||
export async function bindUrl(data): Promise<SysInstallInfo> {
|
||||
export async function bindUrl(data: any): Promise<any> {
|
||||
return await request({
|
||||
url: "/sys/plus/bindUrl",
|
||||
method: "post",
|
||||
|
|
|
@ -47,7 +47,13 @@ function createService() {
|
|||
return dataAxios.data;
|
||||
default:
|
||||
// 不是正确的 code
|
||||
errorCreate(`${dataAxios.msg}: ${response.config.url}`);
|
||||
const errorMessage = dataAxios.msg;
|
||||
// @ts-ignore
|
||||
if (response?.config?.onError) {
|
||||
// @ts-ignore
|
||||
response.config.onError(new Error(errorMessage));
|
||||
}
|
||||
errorCreate(`${errorMessage}: ${response.config.url}`);
|
||||
return dataAxios;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<template>
|
||||
<a-select mode="tags" readonly :value="modelValue" />
|
||||
<div>{{ errorRef }}</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref, watch } from "vue";
|
||||
|
||||
|
@ -57,9 +62,4 @@ watch(
|
|||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-select mode="tags" readonly :value="modelValue" />
|
||||
<div>{{ errorRef }}</div>
|
||||
</template>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
|
@ -23,6 +23,7 @@ export default {
|
|||
|
||||
const pipeline = inject("pipeline") as Ref<any>;
|
||||
const currentStageIndex = inject("currentStageIndex") as Ref<number>;
|
||||
const currentTaskIndex = inject("currentTaskIndex") as Ref<number>;
|
||||
const currentStepIndex = inject("currentStepIndex") as Ref<number>;
|
||||
const currentTask = inject("currentTask") as Ref<any>;
|
||||
|
||||
|
@ -32,6 +33,7 @@ export default {
|
|||
options.value = pluginGroups.getPreStepOutputOptions({
|
||||
pipeline: pipeline.value,
|
||||
currentStageIndex: currentStageIndex.value,
|
||||
currentTaskIndex: currentTaskIndex.value,
|
||||
currentStepIndex: currentStepIndex.value,
|
||||
currentTask: currentTask.value
|
||||
});
|
||||
|
|
|
@ -1,6 +1,28 @@
|
|||
<template>
|
||||
<div class="remote-select">
|
||||
<div class="flex flex-row">
|
||||
<a-select
|
||||
class="remote-select-input"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
:options="optionsRef"
|
||||
:value="value"
|
||||
v-bind="attrs"
|
||||
@click="onClick"
|
||||
@update:value="emit('update:value', $event)"
|
||||
/>
|
||||
<div class="ml-5">
|
||||
<fs-button title="刷新选项" icon="ion:refresh-outline" @click="refreshOptions"></fs-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="helper error">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
|
||||
import { ref, watch } from "vue";
|
||||
import { ref, useAttrs, watch } from "vue";
|
||||
|
||||
defineOptions({
|
||||
name: "RemoteSelect"
|
||||
|
@ -16,6 +38,8 @@ const emit = defineEmits<{
|
|||
"update:value": any;
|
||||
}>();
|
||||
|
||||
const attrs = useAttrs();
|
||||
|
||||
const optionsRef = ref([]);
|
||||
const message = ref("");
|
||||
const getOptions = async () => {
|
||||
|
@ -27,8 +51,8 @@ const getOptions = async () => {
|
|||
input: props.form
|
||||
},
|
||||
{
|
||||
onError(err) {
|
||||
message.value = err.message;
|
||||
onError(err: any) {
|
||||
message.value = `获取选项出错:${err.message}`;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -44,6 +68,10 @@ async function onClick() {
|
|||
return;
|
||||
}
|
||||
isFirst = false;
|
||||
await refreshOptions();
|
||||
}
|
||||
|
||||
async function refreshOptions() {
|
||||
optionsRef.value = await getOptions();
|
||||
}
|
||||
|
||||
|
@ -64,21 +92,4 @@ watch(
|
|||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a-select
|
||||
class="remote-select"
|
||||
show-search
|
||||
:filter-option="filterOption"
|
||||
:options="optionsRef"
|
||||
:value="value"
|
||||
@click="onClick"
|
||||
@update:value="emit('update:value', $event)"
|
||||
/>
|
||||
<div class="helper">
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="less"></style>
|
||||
|
|
|
@ -14,7 +14,7 @@ export type RequestHandleReq<T = any> = {
|
|||
input: T;
|
||||
};
|
||||
|
||||
export async function doRequest(req: RequestHandleReq, opts?: any = {}) {
|
||||
export async function doRequest(req: RequestHandleReq, opts: any = {}) {
|
||||
const url = req.type === "access" ? "/pi/handle/access" : "/pi/handle/plugin";
|
||||
const { typeName, action, data, input } = req;
|
||||
const res = await request({
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div>
|
||||
<contextHolder />
|
||||
<a-input v-bind="attrs" :value="value" :allow-clear="true" @update:value="emit('update:value', $event)">
|
||||
<a-input :value="value" :allow-clear="true" v-bind="attrs" @update:value="emit('update:value', $event)">
|
||||
<template #suffix>
|
||||
<a-tag class="cursor-pointer" @click="getDeviceId">获取设备ID</a-tag>
|
||||
</template>
|
||||
|
|
|
@ -164,9 +164,27 @@ function openUpgrade() {
|
|||
okText: "激活",
|
||||
width: 900,
|
||||
content: () => {
|
||||
let activationCodeGetWay: any = null;
|
||||
if (settingStore.siteEnv.agent.enabled != null) {
|
||||
const agent = settingStore.siteEnv.agent;
|
||||
if (agent.enabled === false) {
|
||||
activationCodeGetWay = (
|
||||
<a href="https://afdian.com/a/greper" target="_blank">
|
||||
爱发电赞助“VIP会员”后获取
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
activationCodeGetWay = (
|
||||
<a href={agent.contactLink} target="_blank">
|
||||
{agent.contactText}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
const vipLabel = settingStore.vipLabel;
|
||||
const slots = [];
|
||||
for (const key in vipTypeDefine) {
|
||||
// @ts-ignore
|
||||
const item = vipTypeDefine[key];
|
||||
const vipBlockClass = `vip-block ${key === settingStore.plusInfo.vipType ? "current" : ""}`;
|
||||
slots.push(
|
||||
|
@ -174,7 +192,7 @@ function openUpgrade() {
|
|||
<div class={vipBlockClass}>
|
||||
<h3 class="block-header">{item.title}</h3>
|
||||
<ul>
|
||||
{item.privilege.map((p) => (
|
||||
{item.privilege.map((p: string) => (
|
||||
<li>
|
||||
<fs-icon class="color-green" icon="ion:checkmark-sharp" />
|
||||
{p}
|
||||
|
@ -203,9 +221,7 @@ function openUpgrade() {
|
|||
|
||||
<div class="mt-10">
|
||||
没有激活码?
|
||||
<a href="https://afdian.com/a/greper" target="_blank">
|
||||
爱发电赞助“VIP会员”后获取
|
||||
</a>
|
||||
{activationCodeGetWay}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -31,7 +31,14 @@
|
|||
<!-- >-->
|
||||
<!-- Button-->
|
||||
<!-- </button>-->
|
||||
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="headerMenus" />
|
||||
<fs-menu
|
||||
v-if="settingStore?.siteEnv?.agent?.enabled === false"
|
||||
class="header-menu"
|
||||
mode="horizontal"
|
||||
:expand-selected="false"
|
||||
:selectable="false"
|
||||
:menus="headerMenus"
|
||||
/>
|
||||
<div class="header-btn">
|
||||
<fs-locale />
|
||||
</div>
|
||||
|
|
|
@ -52,7 +52,7 @@ export const certdResources = [
|
|||
path: "/certd/cname/record",
|
||||
component: "/certd/cname/record/index.vue",
|
||||
meta: {
|
||||
icon: "ion:disc-outline",
|
||||
icon: "ion:link-outline",
|
||||
auth: true
|
||||
}
|
||||
},
|
||||
|
|
|
@ -73,7 +73,7 @@ export const sysResources = [
|
|||
path: "/sys/cname/provider",
|
||||
component: "/sys/cname/provider/index.vue",
|
||||
meta: {
|
||||
icon: "ion:settings-outline",
|
||||
icon: "ion:earth-outline",
|
||||
permission: "sys:settings:view"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -35,8 +35,16 @@ export interface SettingState {
|
|||
};
|
||||
siteInfo: SiteInfo;
|
||||
plusInfo?: PlusInfo;
|
||||
siteEnv?: SiteEnv;
|
||||
}
|
||||
|
||||
export type SiteEnv = {
|
||||
agent?: {
|
||||
enabled?: boolean;
|
||||
contactText?: string;
|
||||
contactLink?: string;
|
||||
};
|
||||
};
|
||||
export type SiteInfo = {
|
||||
title: string;
|
||||
slogan: string;
|
||||
|
@ -94,7 +102,14 @@ export const useSettingStore = defineStore({
|
|||
accountServerBaseUrl: "",
|
||||
appKey: ""
|
||||
},
|
||||
siteInfo: defaultSiteInfo
|
||||
siteInfo: defaultSiteInfo,
|
||||
siteEnv: {
|
||||
agent: {
|
||||
enabled: undefined,
|
||||
contactText: "",
|
||||
contactLink: ""
|
||||
}
|
||||
}
|
||||
}),
|
||||
getters: {
|
||||
getThemeConfig(): any {
|
||||
|
@ -132,6 +147,7 @@ export const useSettingStore = defineStore({
|
|||
},
|
||||
async loadSysSettings() {
|
||||
await this.loadSysPublicSettings();
|
||||
await this.loadSiteEnv();
|
||||
await this.loadInstallInfo();
|
||||
await this.loadPlusInfo();
|
||||
await this.loadSiteInfo();
|
||||
|
@ -145,12 +161,16 @@ export const useSettingStore = defineStore({
|
|||
const installInfo = await basicApi.getInstallInfo();
|
||||
_.merge(this.installInfo, installInfo);
|
||||
},
|
||||
async loadSiteEnv() {
|
||||
const siteEnv = await basicApi.getSiteEnv();
|
||||
_.merge(this.siteEnv, siteEnv);
|
||||
},
|
||||
async loadPlusInfo() {
|
||||
this.plusInfo = await basicApi.getPlusInfo();
|
||||
},
|
||||
async loadSiteInfo() {
|
||||
const isComm = this.isComm;
|
||||
let siteInfo = {};
|
||||
let siteInfo: SiteInfo;
|
||||
if (isComm) {
|
||||
siteInfo = await basicApi.getSiteInfo();
|
||||
if (siteInfo.logo) {
|
||||
|
|
|
@ -203,9 +203,12 @@ h1, h2, h3, h4, h5, h6 {
|
|||
}
|
||||
|
||||
.helper {
|
||||
display: inline-block;
|
||||
color: #aeaeae;
|
||||
font-size: 12px;
|
||||
|
||||
&.error{
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -59,7 +59,10 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
|
|||
show: false
|
||||
},
|
||||
column: {
|
||||
width: 120
|
||||
width: 200,
|
||||
component: {
|
||||
color: "auto"
|
||||
}
|
||||
},
|
||||
form: {
|
||||
component: {
|
||||
|
|
|
@ -51,7 +51,7 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||
key: "id",
|
||||
type: "number",
|
||||
column: {
|
||||
width: 50
|
||||
width: 100
|
||||
},
|
||||
form: {
|
||||
show: false
|
||||
|
@ -65,6 +65,9 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
|
|||
},
|
||||
form: {
|
||||
rules: [{ required: true, message: "必填项" }]
|
||||
},
|
||||
column: {
|
||||
width: 300
|
||||
}
|
||||
},
|
||||
...commonColumnsDefine
|
||||
|
|
|
@ -69,7 +69,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
|
|||
return (
|
||||
<ul>
|
||||
<li>JS-ACME:使用简单方便,功能强大【推荐】</li>
|
||||
<li>Lego-ACME:基于Lego实现,支持海量DNS提供商,熟悉LEGO的用户可以使用【即将废弃】</li>
|
||||
<li>Lego-ACME:基于Lego实现,支持海量DNS提供商,熟悉LEGO的用户可以使用</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
<a-tooltip>
|
||||
<a-button type="dashed" shape="round" @click="taskAdd(stage, index)">
|
||||
<fs-icon class="font-20" icon="ion:add-circle-outline"></fs-icon>
|
||||
并行任务
|
||||
添加任务
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
|
|
|
@ -75,10 +75,11 @@ export class PluginGroups {
|
|||
return this.map[name];
|
||||
}
|
||||
|
||||
getPreStepOutputOptions({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
|
||||
getPreStepOutputOptions({ pipeline, currentStageIndex, currentTaskIndex, currentStepIndex, currentTask }: any) {
|
||||
const steps = this.collectionPreStepOutputs({
|
||||
pipeline,
|
||||
currentStageIndex,
|
||||
currentTaskIndex,
|
||||
currentStepIndex,
|
||||
currentTask
|
||||
});
|
||||
|
@ -96,7 +97,7 @@ export class PluginGroups {
|
|||
return options;
|
||||
}
|
||||
|
||||
collectionPreStepOutputs({ pipeline, currentStageIndex, currentStepIndex, currentTask }: any) {
|
||||
collectionPreStepOutputs({ pipeline, currentStageIndex, currentTaskIndex, currentStepIndex, currentTask }: any) {
|
||||
const steps: any[] = [];
|
||||
// 开始放step
|
||||
for (let i = 0; i < currentStageIndex; i++) {
|
||||
|
@ -107,6 +108,14 @@ export class PluginGroups {
|
|||
}
|
||||
}
|
||||
}
|
||||
//当前阶段之前的task
|
||||
const currentStage = pipeline.stages[currentStageIndex];
|
||||
for (let i = 0; i < currentTaskIndex; i++) {
|
||||
const task = currentStage.tasks[i];
|
||||
for (const step of task.steps) {
|
||||
steps.push(step);
|
||||
}
|
||||
}
|
||||
//放当前任务下的step
|
||||
for (let i = 0; i < currentStepIndex; i++) {
|
||||
const step = currentTask.steps[i];
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<p class="d2-page-cover__sub-title">{{ siteInfo.slogan }}</p>
|
||||
<div v-if="siteInfo.warningOff !== true" class="warning">
|
||||
<div v-if="siteInfo.warningOff !== true && settingStore.siteEnv?.agent?.enabled === false" class="warning">
|
||||
<a-alert type="warning" show-icon>
|
||||
<template #description>
|
||||
<div class="flex">
|
||||
|
|
|
@ -21,19 +21,19 @@ typeorm:
|
|||
dataSource:
|
||||
default:
|
||||
database: './data/db-comm.sqlite'
|
||||
#plus:
|
||||
# server:
|
||||
# baseUrls: ['https://api.ai.handsfree.work', 'https://api.ai.docmirror.cn']
|
||||
#
|
||||
#account:
|
||||
# server:
|
||||
# baseUrl: 'https://ai.handsfree.work/subject'
|
||||
|
||||
|
||||
plus:
|
||||
server:
|
||||
baseUrls: ['http://127.0.0.1:11007']
|
||||
baseUrls: ['https://api.ai.handsfree.work', 'https://api.ai.docmirror.cn']
|
||||
|
||||
account:
|
||||
server:
|
||||
baseUrl: 'http://127.0.0.1:1017/subject'
|
||||
baseUrl: 'https://ai.handsfree.work/subject'
|
||||
|
||||
|
||||
#plus:
|
||||
# server:
|
||||
# baseUrls: ['http://127.0.0.1:11007']
|
||||
#
|
||||
#account:
|
||||
# server:
|
||||
# baseUrl: 'http://127.0.0.1:1017/subject'
|
||||
|
|
|
@ -37,6 +37,12 @@ const development = {
|
|||
'/': '/index.html',
|
||||
},
|
||||
},
|
||||
// '/index.html': {
|
||||
// maxAge: 0,
|
||||
// },
|
||||
// '/': {
|
||||
// maxAge: 0,
|
||||
// },
|
||||
},
|
||||
},
|
||||
cron: {
|
||||
|
@ -109,6 +115,11 @@ const development = {
|
|||
// 仅在匹配路径到 /api/upload 的时候去解析 body 中的文件信息
|
||||
match: /\/api\/basic\/file\/upload/,
|
||||
},
|
||||
agent: {
|
||||
enabled: false,
|
||||
contactText: '',
|
||||
contactLink: '',
|
||||
},
|
||||
} as MidwayConfig;
|
||||
mergeConfig(development, 'development');
|
||||
|
||||
|
|
|
@ -54,7 +54,11 @@ export class AutoInitSite {
|
|||
}
|
||||
|
||||
// 授权许可
|
||||
await this.plusService.verify();
|
||||
try {
|
||||
await this.plusService.verify();
|
||||
} catch (e) {
|
||||
logger.error('授权许可验证失败', e);
|
||||
}
|
||||
|
||||
logger.info('初始化站点完成');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { ALL, Body, Config, Controller, Get, Inject, Provide } from '@midwayjs/core';
|
||||
import { BaseController, Constants, SysInstallInfo, SysPublicSettings, SysSettingsService, SysSiteInfo } from '@certd/lib-server';
|
||||
import { BaseController, Constants, SysInstallInfo, SysPublicSettings, SysSettingsService, SysSiteEnv, SysSiteInfo } from '@certd/lib-server';
|
||||
import { AppKey, getPlusInfo } from '@certd/pipeline';
|
||||
|
||||
/**
|
||||
|
@ -12,6 +12,9 @@ export class BasicSettingsController extends BaseController {
|
|||
@Config('account.server.baseUrl')
|
||||
accountServerBaseUrl: any;
|
||||
|
||||
@Config('agent')
|
||||
agentConfig: SysSiteEnv['agent'];
|
||||
|
||||
@Get('/public', { summary: Constants.per.guest })
|
||||
public async getSysPublic() {
|
||||
const settings = await this.sysSettingsService.getSetting(SysPublicSettings);
|
||||
|
@ -32,6 +35,14 @@ export class BasicSettingsController extends BaseController {
|
|||
return this.ok(settings);
|
||||
}
|
||||
|
||||
@Get('/siteEnv', { summary: Constants.per.guest })
|
||||
public async getSiteEnv() {
|
||||
const env: SysSiteEnv = {
|
||||
agent: this.agentConfig,
|
||||
};
|
||||
return this.ok(env);
|
||||
}
|
||||
|
||||
@Get('/plusInfo', { summary: Constants.per.guest })
|
||||
async plusInfo(@Body(ALL) body: any) {
|
||||
const info = getPlusInfo();
|
||||
|
|
|
@ -62,9 +62,10 @@ export class PipelineController extends CrudController<PipelineService> {
|
|||
|
||||
@Post('/save', { summary: Constants.per.authOnly })
|
||||
async save(@Body(ALL) bean: PipelineEntity) {
|
||||
bean.userId = this.getUserId();
|
||||
if (bean.id > 0) {
|
||||
await this.authService.checkEntityUserId(this.ctx, this.getService(), bean.id);
|
||||
} else {
|
||||
bean.userId = this.getUserId();
|
||||
}
|
||||
await this.service.save(bean);
|
||||
return this.ok(bean.id);
|
||||
|
|
|
@ -118,10 +118,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||
const isUpdate = bean.id > 0 && old != null;
|
||||
if (!isPlus()) {
|
||||
let count = await this.repository.count();
|
||||
if (isUpdate) {
|
||||
count -= 1;
|
||||
if (!isUpdate) {
|
||||
//如果是添加要加1
|
||||
count += 1;
|
||||
}
|
||||
if (count >= freeCount) {
|
||||
if (count > freeCount) {
|
||||
throw new NeedVIPException('免费版最多只能创建10个pipeline');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,3 +10,4 @@ export * from './plugin-west/index.js';
|
|||
export * from './plugin-doge/index.js';
|
||||
export * from './plugin-qiniu/index.js';
|
||||
export * from './plugin-jdcloud/index.js';
|
||||
export * from './plugin-woai/index.js';
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// @ts-ignore
|
||||
import ssh2, { ConnectConfig } from 'ssh2';
|
||||
import ssh2, { ConnectConfig, ExecOptions } from 'ssh2';
|
||||
import path from 'path';
|
||||
import * as _ from 'lodash-es';
|
||||
import { ILogger } from '@certd/pipeline';
|
||||
|
@ -269,7 +269,7 @@ export class SshClient {
|
|||
* Set-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\cmd.exe"
|
||||
* @param options
|
||||
*/
|
||||
async exec(options: { connectConf: SshAccess; script: string | Array<string> }) {
|
||||
async exec(options: { connectConf: SshAccess; script: string | Array<string>; env?: any }): Promise<string[]> {
|
||||
let { script } = options;
|
||||
const { connectConf } = options;
|
||||
|
||||
|
@ -278,14 +278,32 @@ export class SshClient {
|
|||
connectConf,
|
||||
callable: async (conn: AsyncSsh2Client) => {
|
||||
let isWinCmd = false;
|
||||
const isLinux = !connectConf.windows;
|
||||
const envScripts = [];
|
||||
if (connectConf.windows) {
|
||||
isWinCmd = await this.isCmd(conn);
|
||||
}
|
||||
|
||||
if (options.env) {
|
||||
for (const key in options.env) {
|
||||
if (isLinux) {
|
||||
envScripts.push(`export ${key}=${options.env[key]}`);
|
||||
} else if (isWinCmd) {
|
||||
//win cmd
|
||||
envScripts.push(`set ${key}=${options.env[key]}`);
|
||||
} else {
|
||||
//powershell
|
||||
envScripts.push(`$env:${key}="${options.env[key]}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isWinCmd) {
|
||||
//组合成&&的形式
|
||||
if (typeof script === 'string') {
|
||||
script = script.split('\n');
|
||||
}
|
||||
script = envScripts.concat(script);
|
||||
script = script as Array<string>;
|
||||
script = script.join(' && ');
|
||||
} else {
|
||||
|
@ -293,6 +311,9 @@ export class SshClient {
|
|||
script = script as Array<string>;
|
||||
script = script.join('\n');
|
||||
}
|
||||
if (envScripts.length > 0) {
|
||||
script = envScripts.join('\n') + '\n' + script;
|
||||
}
|
||||
}
|
||||
await conn.exec(script);
|
||||
},
|
||||
|
|
|
@ -3,6 +3,7 @@ import { SshClient } from '../../lib/ssh.js';
|
|||
import { CertInfo, CertReader, CertReaderHandleContext } from '@certd/plugin-cert';
|
||||
import * as fs from 'fs';
|
||||
import { SshAccess } from '../../access/index.js';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'uploadCertToHost',
|
||||
|
@ -106,6 +107,18 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||
})
|
||||
script!: string;
|
||||
|
||||
@TaskInput({
|
||||
title: '注入环境变量',
|
||||
value: false,
|
||||
component: {
|
||||
name: 'a-switch',
|
||||
vModel: 'checked',
|
||||
},
|
||||
helper: '是否将证书域名、路径等信息注入脚本执行环境变量中,具体的变量名称,可以运行后从日志中查看',
|
||||
required: false,
|
||||
})
|
||||
injectEnv!: string;
|
||||
|
||||
@TaskOutput({
|
||||
title: '证书保存路径',
|
||||
})
|
||||
|
@ -233,10 +246,28 @@ export class UploadCertToHostPlugin extends AbstractTaskPlugin {
|
|||
const connectConf: SshAccess = await this.accessService.getById(accessId);
|
||||
const sshClient = new SshClient(this.logger);
|
||||
this.logger.info('执行脚本命令');
|
||||
|
||||
//环境变量
|
||||
const env = {};
|
||||
if (this.injectEnv) {
|
||||
const domains = certReader.getAllDomains();
|
||||
for (let i = 0; i < domains.length; i++) {
|
||||
env[`CERT_DOMAIN_${i}`] = domains[i];
|
||||
}
|
||||
env['CERT_EXPIRES'] = dayjs(certReader.getCrtDetail().expires).unix();
|
||||
|
||||
env['HOST_CRT_PATH'] = this.hostCrtPath || '';
|
||||
env['HOST_KEY_PATH'] = this.hostKeyPath || '';
|
||||
env['HOST_IC_PATH'] = this.hostIcPath || '';
|
||||
env['HOST_PFX_PATH'] = this.hostPfxPath || '';
|
||||
env['HOST_DER_PATH'] = this.hostDerPath || '';
|
||||
}
|
||||
|
||||
const scripts = this.script.split('\n');
|
||||
await sshClient.exec({
|
||||
connectConf,
|
||||
script: scripts,
|
||||
env,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,9 +18,17 @@ export class QiniuDeployCertToCDN extends AbstractTaskPlugin {
|
|||
@TaskInput({
|
||||
title: 'CDN加速域名',
|
||||
helper: '你在七牛云上配置的CDN加速域名,比如:certd.handsfree.work',
|
||||
component: {
|
||||
name: 'a-select',
|
||||
vModel: 'value',
|
||||
mode: 'tags',
|
||||
open: false,
|
||||
tokenSeparators: [',', ' ', ',', '、', '|'],
|
||||
},
|
||||
rules: [{ type: 'domains' }],
|
||||
required: true,
|
||||
})
|
||||
domainName!: string;
|
||||
domainName!: string | string[];
|
||||
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
|
@ -52,7 +60,7 @@ export class QiniuDeployCertToCDN extends AbstractTaskPlugin {
|
|||
http: this.ctx.http,
|
||||
access,
|
||||
});
|
||||
const url = `https://api.qiniu.com/domain/${this.domainName}/httpsconf`;
|
||||
|
||||
let certId = null;
|
||||
if (typeof this.cert !== 'string') {
|
||||
// 是证书id,直接上传即可
|
||||
|
@ -62,13 +70,17 @@ export class QiniuDeployCertToCDN extends AbstractTaskPlugin {
|
|||
certId = this.cert;
|
||||
}
|
||||
|
||||
//开始修改证书
|
||||
this.logger.info(`开始修改证书,certId:${certId},domain:${this.domainName}`);
|
||||
const body = {
|
||||
certID: certId,
|
||||
};
|
||||
|
||||
await qiniuClient.doRequest(url, 'put', body);
|
||||
const domains: string[] = typeof this.domainName === 'string' ? [this.domainName] : this.domainName;
|
||||
for (const domain of domains) {
|
||||
//开始修改证书
|
||||
this.logger.info(`开始修改证书,certId:${certId},domain:${domain}`);
|
||||
const body = {
|
||||
certID: certId,
|
||||
};
|
||||
const url = `https://api.qiniu.com/domain/${domain}/httpsconf`;
|
||||
await qiniuClient.doRequest(url, 'put', body);
|
||||
this.logger.info(`修改证书成功,certId:${certId},domain:${domain}`);
|
||||
}
|
||||
|
||||
this.logger.info('部署完成');
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { AccessInput, BaseAccess, IsAccess } from '@certd/pipeline';
|
||||
|
||||
@IsAccess({
|
||||
name: 'woai',
|
||||
title: '我爱云授权',
|
||||
desc: '我爱云CDN',
|
||||
})
|
||||
export class WoaiAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: '账号',
|
||||
component: {
|
||||
placeholder: '我爱云的账号',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
username = '';
|
||||
@AccessInput({
|
||||
title: '密码',
|
||||
component: {
|
||||
placeholder: '我爱云的密码',
|
||||
},
|
||||
required: true,
|
||||
encrypt: true,
|
||||
})
|
||||
password = '';
|
||||
}
|
||||
|
||||
new WoaiAccess();
|
|
@ -0,0 +1,2 @@
|
|||
export * from './plugins/index.js';
|
||||
export * from './access.js';
|
|
@ -0,0 +1 @@
|
|||
export * from './plugin-deploy-to-cdn.js';
|
|
@ -0,0 +1,94 @@
|
|||
import { AbstractTaskPlugin, HttpClient, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
|
||||
import { CertInfo } from '@certd/plugin-cert';
|
||||
import { WoaiAccess } from '../access.js';
|
||||
|
||||
@IsTaskPlugin({
|
||||
name: 'WoaiCDN',
|
||||
title: '部署证书到我爱云 CDN',
|
||||
desc: '部署证书到我爱云CDN',
|
||||
icon: 'clarity:plugin-line',
|
||||
group: pluginGroups.cdn.key,
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
export class WoaiCdnPlugin extends AbstractTaskPlugin {
|
||||
@TaskInput({
|
||||
title: '证书ID',
|
||||
helper: '请填写 [证书列表](https://console.edge.51vs.club/site/certificate) 中的证书的ID',
|
||||
component: { name: 'a-input' },
|
||||
required: true,
|
||||
})
|
||||
certId!: string;
|
||||
@TaskInput({
|
||||
title: '域名证书',
|
||||
helper: '请选择前置任务输出的域名证书',
|
||||
component: {
|
||||
name: 'output-selector',
|
||||
from: ['CertApply', 'CertApplyLego'],
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
cert!: CertInfo;
|
||||
@TaskInput({
|
||||
title: 'Access授权',
|
||||
helper: '我爱云的用户、密码授权',
|
||||
component: {
|
||||
name: 'access-selector',
|
||||
type: 'woai',
|
||||
},
|
||||
required: true,
|
||||
})
|
||||
accessId!: string;
|
||||
http!: HttpClient;
|
||||
private readonly baseApi = 'https://console.edeg.51vs.club';
|
||||
|
||||
async onInstance() {
|
||||
this.http = this.ctx.http;
|
||||
}
|
||||
|
||||
private async doRequestApi(url: string, data: any = null, method = 'post', token: string | null = null) {
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { Token: token } : {}),
|
||||
};
|
||||
const res = await this.http.request<any, any>({
|
||||
url,
|
||||
method,
|
||||
data,
|
||||
headers,
|
||||
});
|
||||
if (res.code !== 200) {
|
||||
throw new Error(`${JSON.stringify(res.message)}`);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
const { certId, cert, accessId } = this;
|
||||
const access = (await this.accessService.getById(accessId)) as WoaiAccess;
|
||||
// 登录获取token
|
||||
const loginResponse = await this.doRequestApi(`${this.baseApi}/account/login`, {
|
||||
username: access.username,
|
||||
password: access.password,
|
||||
});
|
||||
const token = loginResponse.data.token;
|
||||
this.logger.info('登录成功,获取到Token:', token);
|
||||
// 更新证书
|
||||
const editCertResponse = await this.doRequestApi(
|
||||
`${this.baseApi}/certificate/edit`,
|
||||
{
|
||||
id: certId,
|
||||
cert: cert.crt,
|
||||
key: cert.key,
|
||||
},
|
||||
'post',
|
||||
token
|
||||
);
|
||||
this.logger.info('证书更新成功:', editCertResponse.message);
|
||||
}
|
||||
}
|
||||
|
||||
new WoaiCdnPlugin();
|
|
@ -1,5 +1,5 @@
|
|||
packages:
|
||||
# all packages in subdirs of packages/ and components/
|
||||
- 'packages/**'
|
||||
# exclude packages that are inside test directories
|
||||
- 'packages/**' # <--------------开发插件请注释掉这一行,PR时本修改不要提交
|
||||
- 'packages/ui/**'
|
||||
- '!**/test/**'
|
||||
|
||||
|
|
Loading…
Reference in New Issue