Merge remote-tracking branch 'origin/v2-dev' into v2-dev

pull/213/head
xiaojunnuo 2024-10-10 21:44:49 +08:00
commit 79f8e5bf47
45 changed files with 684 additions and 101 deletions

View File

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

View File

@ -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"
```
## 重启服务进行调试

View File

@ -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、 其他就跟正常申请证书一样了

View File

@ -54,3 +54,11 @@ export class SysSiteInfo extends BaseSettings {
logo?: string;
loginLogo?: string;
}
export class SysSiteEnv {
agent?: {
enabled?: boolean;
contactText?: string;
contactLink?: string;
};
}

View File

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

View File

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

View File

@ -1 +1,2 @@
export * from "./eab-access.js";
export * from "./google-access.js";

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
FROM greper/certd:latest
ENV certd_agent_enabled=true
CMD ["npm", "run","start"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -203,9 +203,12 @@ h1, h2, h3, h4, h5, h6 {
}
.helper {
display: inline-block;
color: #aeaeae;
font-size: 12px;
&.error{
color: #ff4d4f;
}
}

View File

@ -59,7 +59,10 @@ export function getCommonColumnDefine(crudExpose: any, typeRef: any) {
show: false
},
column: {
width: 120
width: 200,
component: {
color: "auto"
}
},
form: {
component: {

View File

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

View File

@ -69,7 +69,7 @@ export default function (certPluginGroup: PluginGroup, formWrapperRef: any): Cre
return (
<ul>
<li>JS-ACME使便</li>
<li>Lego-ACMELegoDNSLEGO使</li>
<li>Lego-ACMELegoDNSLEGO使</li>
</ul>
);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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');

View File

@ -54,7 +54,11 @@ export class AutoInitSite {
}
// 授权许可
await this.plusService.verify();
try {
await this.plusService.verify();
} catch (e) {
logger.error('授权许可验证失败', e);
}
logger.info('初始化站点完成');
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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('部署完成');
}

View File

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

View File

@ -0,0 +1,2 @@
export * from './plugins/index.js';
export * from './access.js';

View File

@ -0,0 +1 @@
export * from './plugin-deploy-to-cdn.js';

View File

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

View File

@ -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/**'