Compare commits

..

16 Commits

Author SHA1 Message Date
xiaojunnuo 431afd618f perf: 企业模式下面增加个人数据迁移的引导 2026-03-22 00:49:54 +08:00
xiaojunnuo 6d5e5bd692 chore: 优化access edit 请求参数,删除多余的参数 2026-03-21 23:59:11 +08:00
xiaojunnuo ffd2e8149e perf: 火山引擎部署alb证书插件支持部署扩展证书以及删除已过期扩展证书 2026-03-21 23:51:30 +08:00
xiaojunnuo 2ab92dc13e chore: cnb_sync 2026-03-20 18:14:31 +08:00
xiaojunnuo 7f6a8bc87e perf: 优化远程数据选择框,选择数据时不刷新闪烁 2026-03-20 18:04:13 +08:00
xiaojunnuo b1ff163a28 fix: 修复模版id不正确导致修改到错误的模版流水线bug 2026-03-20 17:48:47 +08:00
xiaojunnuo 440d55ccb8 chore: sync push 2026-03-20 13:53:12 +08:00
xiaojunnuo 285532d431 fix: remote-select默认pageSize设置为50,阿里云WAF不支持pageSize100 2026-03-20 13:44:49 +08:00
xiaojunnuo f2c47459f8 Merge branch 'v2' of https://github.com/certd/certd into v2 2026-03-20 12:09:01 +08:00
xiaojunnuo 49703f08e5 fix: 修复批量执行按钮无效的bug 2026-03-20 12:08:55 +08:00
xiaojunnuo 1d0aa9573b Merge branch 'v2' of https://github.com/certd/certd into v2 2026-03-19 01:12:34 +08:00
xiaojunnuo b2014cf88b build: release 2026-03-19 01:11:46 +08:00
xiaojunnuo a0e0078bad build: prepare to build 2026-03-19 01:09:00 +08:00
xiaojunnuo 5ebca21c32 build: publish 2026-03-19 00:52:27 +08:00
xiaojunnuo 970aea90c9 build: trigger build image 2026-03-19 00:52:15 +08:00
xiaojunnuo 3332d2288f chore: 支持推送到cnb 2026-03-18 16:16:22 +08:00
36 changed files with 374 additions and 88 deletions
@@ -17,6 +17,7 @@ jobs:
with:
fetch-depth: 0
lfs: true
ref: v2-dev
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
run: |
git config --global user.name "xiaojunnuo"
+1
View File
@@ -17,6 +17,7 @@ jobs:
with:
fetch-depth: 0
lfs: true
ref: v2
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
run: |
git config --global user.name "xiaojunnuo"
+35
View File
@@ -0,0 +1,35 @@
name: sync-to-cnb-dev
on:
push:
branches: ['v2-dev']
# schedule:
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout work repo # 1. 检出当前仓库(certd-sync-work)
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
ref: v2-dev
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
run: |
git config --global user.name "xiaojunnuo"
git config --global user.email "xiaojunnuo@qq.com"
- name: Set git token # 3. 给git命令设置token,用于push到目标仓库
uses: de-vri-es/setup-git-credentials@v2
with: # token 格式为: username:password
credentials: https://cnb:${{secrets.CNB_TOKEN}}@cnb.cool
- name: push to cnb # 4. 执行同步
run: |
git remote add upstream https://cnb.cool/certd/certd.git
git push --set-upstream upstream v2-dev
+34
View File
@@ -0,0 +1,34 @@
name: sync-to-cnb
on:
push:
branches: ['v2']
# schedule:
# - # 国际时间 19:17 执行,北京时间3:17 ↙↙↙ 改成你想要每天自动执行的时间
# - cron: '17 19 * * *'
permissions:
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- name: Checkout work repo # 1. 检出当前仓库(certd-sync-work)
uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
run: |
git config --global user.name "xiaojunnuo"
git config --global user.email "xiaojunnuo@qq.com"
- name: Set git token # 3. 给git命令设置token,用于push到目标仓库
uses: de-vri-es/setup-git-credentials@v2
with: # token 格式为: username:password
credentials: https://cnb:${{secrets.CNB_TOKEN}}@cnb.cool
- name: push to cnb # 4. 执行同步
run: |
git remote add upstream https://cnb.cool/certd/certd.git
git push --set-upstream upstream v2
+1
View File
@@ -17,6 +17,7 @@ jobs:
with:
fetch-depth: 0
lfs: true
ref: v2-dev
- name: Set git user # 2. 给git命令设置用户名和邮箱,↙↙↙ 改成你的name和email
run: |
git config --global user.name "xiaojunnuo"
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.5](https://github.com/certd/certd/compare/v1.39.4...v1.39.5) (2026-03-18)
### Bug Fixes
* 修复修改分组报错的bug ([224db7d](https://github.com/certd/certd/commit/224db7da57dbdddf25bcac7faa0a29eb228c5a33))
### Performance Improvements
* 移除passkey的counter递增校验 ([68b669d](https://github.com/certd/certd/commit/68b669d3ff3e13b931939093320ce7237bb02b1b))
* passkey 支持Bitwarden ([29f44c6](https://github.com/certd/certd/commit/29f44c67c808bed9ff1c9d4884d39a1a62d043a7))
* passkey登录放到下方其他登录位置 ([1413e1a](https://github.com/certd/certd/commit/1413e1aff4aabcfd471716338c210fbcfd76c8f9))
## [1.39.4](https://github.com/certd/certd/compare/v1.39.3...v1.39.4) (2026-03-17)
### Bug Fixes
+1 -1
View File
@@ -70,5 +70,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -1 +1 @@
00:48
01:09
+1 -1
View File
@@ -47,5 +47,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -56,5 +56,5 @@
"fetch"
]
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -64,5 +64,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
+1 -1
View File
@@ -57,5 +57,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "aacee4a94c2e2481be8a307fb5a6a5d04a09dda7"
"gitHead": "5f9341ad8edd08bb553c2d04e71f0838032e727f"
}
@@ -104,7 +104,7 @@ const loading = ref(false);
const pagerRef: Ref = ref({
pageNo: 1,
total: 0,
pageSize: props.pageSize || 100,
pageSize: props.pageSize || 50,
});
const getOptions = async () => {
if (loading.value) {
@@ -182,7 +182,7 @@ const getOptions = async () => {
pagerRef.value.pageNo = res.pageNo ?? 1;
}
if (res.pageSize != null) {
pagerRef.value.pageSize = res.pageSize ?? 100;
pagerRef.value.pageSize = res.pageSize ?? pageSize;
}
if (res.total != null) {
pagerRef.value.total = res.total ?? list.length;
@@ -235,9 +235,10 @@ watch(
const { form } = value;
const oldForm: any = oldValue?.form;
let changed = oldForm == null || optionsRef.value.length == 0;
debugger;
if (props.watches && props.watches.length > 0) {
for (const key of props.watches) {
if (oldForm && form[key] != oldForm[key]) {
if (oldForm && JSON.stringify(form[key]) != JSON.stringify(oldForm[key])) {
changed = true;
break;
}
@@ -795,6 +795,8 @@ export default {
reverseProxyEmpty: "No reverse proxy list configured",
environmentVars: "Environment Variables",
environmentVarsHelper: "configure the runtime environment variables, one per line, format: KEY=VALUE",
bindUrl: "Bind URL",
},
},
modal: {
@@ -804,6 +804,7 @@ export default {
reverseProxyEmpty: "未配置反向代理",
environmentVars: "环境变量",
environmentVarsHelper: "配置运行时环境变量,每行一个,格式:KEY=VALUE",
bindUrl: "绑定URL",
},
},
modal: {
@@ -272,14 +272,27 @@ export const useSettingStore = defineStore({
},
async checkUrlBound() {
const userStore = useUserStore();
const settingStore = useSettingStore();
if (!userStore.isAdmin) {
return;
}
const bindUrl = this.installInfo.bindUrl;
const bindUrl2 = this.installInfo.bindUrl2;
if (!bindUrl) {
//绑定url
await this.doBindUrl("url");
} else {
//检查当前url 是否与绑定的url一致
const url = window.location.href;
if (!url.startsWith(bindUrl) && !url.startsWith(bindUrl2)) {
this.openBindUrlModal();
}
}
},
openBindUrlModal() {
const event: any = { ModalRef: null };
mitter.emit("getModal", event);
const Modal = event.ModalRef;
let modalRef: any = null;
const bindUrl = this.installInfo.bindUrl;
const bindUrl2 = this.installInfo.bindUrl2;
@@ -289,57 +302,47 @@ export const useSettingStore = defineStore({
modalRef.destroy();
}
};
if (!bindUrl) {
//绑定url
await this.doBindUrl("url");
} else {
//检查当前url 是否与绑定的url一致
const url = window.location.href;
if (!url.startsWith(bindUrl) && !url.startsWith(bindUrl2)) {
modalRef = Modal.warning({
title: "URL地址未绑定,是否绑定此地址?",
width: 500,
keyboard: false,
content: () => {
return (
<div class="p-4">
<div class="flex items-center justify-between">
<span>
1
<a-tag color="green">{bindUrl || "未占用"}</a-tag>
</span>
<a-button type="primary" onClick={() => doBindRequest("url")}>
1
</a-button>
</div>
<div class="flex items-center justify-between mt-3">
<span>
2
<a-tag color="green">{bindUrl2 || "未占用"}</a-tag>
</span>
<a-button type="primary" onClick={() => doBindRequest("url2")}>
2
</a-button>
</div>
</div>
);
},
onOk: async () => {
// await this.doBindUrl();
window.location.href = bindUrl;
},
okButtonProps: {
danger: true,
},
okText: "不,回到原来的地址",
cancelText: "不,回到原来的地址",
onCancel: () => {
window.location.href = bindUrl;
},
});
}
}
const modalRef: any = Modal.warning({
title: "URL地址未绑定,是否绑定此地址?",
width: 500,
keyboard: false,
content: () => {
return (
<div class="p-4">
<div class="flex items-center justify-between">
<span>
1
<a-tag color="green">{bindUrl || "未占用"}</a-tag>
</span>
<a-button type="primary" onClick={() => doBindRequest("url")}>
1
</a-button>
</div>
<div class="flex items-center justify-between mt-3">
<span>
2
<a-tag color="green">{bindUrl2 || "未占用"}</a-tag>
</span>
<a-button type="primary" onClick={() => doBindRequest("url2")}>
2
</a-button>
</div>
</div>
);
},
onOk: async () => {
// await this.doBindUrl();
window.location.href = bindUrl;
},
okButtonProps: {
danger: true,
},
okText: "不,回到原来的地址",
cancelText: "不,回到原来的地址",
onCancel: () => {
window.location.href = bindUrl;
},
});
},
async loadProductInfo() {
try {
@@ -18,6 +18,8 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { form, row } = req;
form.id = row.id;
form.type = props.type;
delete form.access;
delete form.keyId;
const res = await context.api.UpdateObj(form);
lastResRef.value = res;
return res;
@@ -30,6 +32,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const addRequest = async (req: AddReq) => {
const { form } = req;
form.type = props.type;
delete form.access;
const res = await context.api.AddObj(form);
lastResRef.value = res;
return res;
@@ -70,6 +73,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
width: "1050px",
},
},
rowHandle: {
width: 200,
},
@@ -89,6 +93,9 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
}, // 点击行
};
},
remove: {
confirmMessage: "授权如果已经被使用,可能会导致流水线无法正常运行,请谨慎操作",
},
},
columns: {
id: {
@@ -15,6 +15,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
delete form.access;
const res = await api.UpdateObj(form);
return res;
};
@@ -25,6 +26,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const addRequest = async (req: AddReq) => {
const { form } = req;
delete form.access;
const res = await api.AddObj(form);
return res;
};
@@ -16,6 +16,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
delete form.body;
const res = await api.UpdateObj(form);
return res;
};
@@ -26,6 +27,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const addRequest = async (req: AddReq) => {
const { form } = req;
delete form.body;
const res = await api.AddObj(form);
return res;
};
@@ -11,6 +11,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
delete form.body;
const res = await api.UpdateObj(form);
return res;
};
@@ -21,6 +22,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const addRequest = async (req: AddReq) => {
const { form } = req;
delete form.body;
const res = await api.AddObj(form);
return res;
};
@@ -14,6 +14,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const editRequest = async (req: EditReq) => {
const { form, row } = req;
form.id = row.id;
delete form.body;
const res = await context.api.UpdateObj(form);
lastResRef.value = res;
return res;
@@ -25,6 +26,7 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const addRequest = async (req: AddReq) => {
const { form } = req;
delete form.body;
const res = await context.api.AddObj(form);
lastResRef.value = res;
return res;
@@ -29,15 +29,15 @@ const pipelineOptions: PipelineOptions = {
onLoaded(detail);
return {
pipeline: {
id: detail.pipeline.id,
stages: [],
triggers: [],
...JSON.parse(detail.pipeline.content || "{}"),
type: detail.pipeline.type,
from: detail.pipeline.from,
id: detail.pipeline.id,
userId: detail.pipeline.userId,
projectId: detail.pipeline.projectId,
},
type: detail.pipeline.type,
from: detail.pipeline.from,
validTime: detail.pipeline.validTime,
webhookKey: detail.pipeline.webhookKey,
id: detail.pipeline.id,
@@ -49,6 +49,7 @@ defineOptions({
const route = useRoute();
const projectIdStr = route.query.projectId as string;
const migrate = route.query.migrate as string;
let projectId = Number(projectIdStr);
const projectStore = useProjectStore();
if (!projectId) {
@@ -116,7 +117,11 @@ onMounted(async () => {
return;
}
await loadProjectDetail();
crudExpose.doRefresh();
await crudExpose.doRefresh();
if (migrate === "true") {
openTransferDialog();
}
});
onActivated(async () => {
await crudExpose.doRefresh();
@@ -8,9 +8,10 @@
</div>
<div class="helper">SaaS模式每个用户管理自己的流水线和授权资源独立使用</div>
<div class="helper">企业模式通过项目合作管理流水线证书和授权资源所有用户视为企业内部员工</div>
<div class="helper"><a @click="adminModeIntroOpen = true">更多管理模式介绍</a></div>
<div class="helper text-red-500">建议在开始使用时固定一个合适的模式之后就不要随意切换了</div>
<div v-if="formState.public.adminMode === 'enterprise'" class="helper">设置为企业模式之后,之前创建的个人数据不会显示,您可以选择<a @click="goCurrentProject"> 将个人数据迁移到项目</a></div>
<div v-if="settingsStore.isComm" class="helper text-red-500">商业版不建议设置为企业模式除非你确定要转成企业内部使用</div>
<div><a @click="adminModeIntroOpen = true"> 更多管理模式介绍</a></div>
</a-form-item>
<a-form-item label=" " :colon="false" :wrapper-col="{ span: 8 }">
@@ -33,6 +34,7 @@ import { useI18n } from "/src/locales";
import { dict } from "@fast-crud/fast-crud";
import { useProjectStore } from "/@/store/project";
import AdminModeIntro from "/@/views/sys/enterprise/project/intro.vue";
import { useRouter } from "vue-router";
const { t } = useI18n();
defineOptions({
@@ -82,5 +84,15 @@ const onFinish = async (form: any) => {
saveLoading.value = false;
}
};
const router = useRouter();
const goCurrentProject = () => {
router.push({
path: "/certd/project/detail",
query: {
migrate: "true",
},
});
};
</script>
<style lang="less"></style>
@@ -8,7 +8,7 @@
</div>
<pre class="helper pre">{{ t("certd.sys.setting.passkeyEnabledHelper", [bindDomain]) }}</pre>
<div v-if="!bindDomainIsSame" class="text-red-500 text-sm mt-2">
{{ t("certd.sys.setting.passkeyHostnameNotSame") }}
{{ t("certd.sys.setting.passkeyHostnameNotSame") }} <a-button class="ml-2" size="small" type="primary" @click="settingsStore.openBindUrlModal()">{{ t("certd.sys.setting.bindUrl") }}</a-button>
</div>
</a-form-item>
<a-form-item :label="t('certd.sys.setting.enableOauth')" :name="['public', 'oauthEnabled']">
@@ -274,6 +274,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
RunnableCollection.initPipelineRunnableType(pipeline);
pipeline.userId = bean.userId;
pipeline.projectId = bean.projectId;
if (bean.id) {
pipeline.id = bean.id;
}
let domains = [];
if (pipeline.stages) {
RunnableCollection.each(pipeline.stages, (runnable: any) => {
@@ -1074,6 +1077,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级专业版");
}
//允许管理员修改,userId=null
const query: any = {}
if (userId && userId > 0) {
query.userId = userId;
@@ -1126,6 +1130,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级专业版");
}
//允许管理员修改,userId=null
const query: any = {}
if (userId && userId > 0) {
query.userId = userId;
@@ -1164,9 +1169,9 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级专业版");
}
if (userId == null || ids.length === 0) {
return;
//允许userId为空,为空则为管理员触发
if (ids.length === 0) {
throw new Error("参数错误 ids 不能为空");
}
const where: any = {
id: In(ids),
@@ -3,6 +3,7 @@ import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from
import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
import { VolcengineAccess } from "../access.js";
import { VolcengineClient } from "../ve-client.js";
import dayjs from "dayjs";
@IsTaskPlugin({
name: "VolcengineDeployToALB",
@@ -32,6 +33,7 @@ export class VolcengineDeployToALB extends AbstractTaskPlugin {
certDomains!: string[];
@TaskInput({
title: "Access授权",
helper: "火山引擎AccessKeyId、AccessKeySecret",
@@ -126,6 +128,22 @@ export class VolcengineDeployToALB extends AbstractTaskPlugin {
listenerList!: string | string[];
@TaskInput({
title: "证书部署类型",
helper: "选择部署默认证书还是扩展证书",
component: {
name: "a-select",
options: [
{ label: "默认证书", value: "default" },
{ label: "扩展证书", value: "extension" }
]
},
value: "default",
required: true
})
certType!: string;
async onInstance() {
}
@@ -149,20 +167,101 @@ export class VolcengineDeployToALB extends AbstractTaskPlugin {
const service = await this.getAlbService();
for (const listener of this.listenerList) {
this.logger.info(`开始部署监听器${listener}证书`);
await service.request({
action: "ModifyListenerAttributes",
query: {
ListenerId: listener,
CertificateSource: "cert_center",
CertCenterCertificateId: certId
}
});
this.logger.info(`部署监听器${listener}证书成功`);
if (this.certType === "default") {
// 部署默认证书
const res = await service.request({
action: "ModifyListenerAttributes",
query: {
ListenerId: listener,
CertificateSource: "cert_center",
CertCenterCertificateId: certId
}
});
this.logger.info(`部署监听器${listener}默认证书成功,res:${JSON.stringify(res)}`);
} else {
// 部署扩展证书
await this.deployExtensionCertificate(service, listener, certId as string);
}
await this.ctx.utils.sleep(5000);
}
this.logger.info("部署完成");
}
private async deployExtensionCertificate(service: any, listenerId: string, certId: string) {
// 获取监听器当前的扩展证书列表
const domainExtensions = await this.getListenerDomainExtensions(service, listenerId);
// 删除过期的扩展证书
try {
await this.deleteExpiredExtensions(service, listenerId, domainExtensions);
} catch (error) {
this.logger.error(`删除过期扩展证书失败:${error.message ||error}`);
}
// 新增扩展证书
const query: any = {
ListenerId: listenerId,
"DomainExtensions.1.Action": "create",
"DomainExtensions.1.CertificateSource": "cert_center",
"DomainExtensions.1.CertCenterCertificateId": certId
};
// 如果有证书域名信息,添加到扩展证书中
if (this.certDomains && this.certDomains.length > 0) {
query["DomainExtensions.1.Domain"] = this.certDomains[0];
}
await service.request({
action: "ModifyListenerAttributes",
query: query
});
this.logger.info(`部署监听器${listenerId}扩展证书成功`);
}
private async getListenerDomainExtensions(service: any, listenerId: string): Promise<any[]> {
const res = await service.request({
action: "DescribeListenerAttributes",
method: "GET",
query: {
ListenerId: listenerId
}
});
return res.Result.DomainExtensions || [];
}
private async deleteExpiredExtensions(service: any, listenerId: string, domainExtensions: any[]) {
const expiredExtensions = [];
for (const ext of domainExtensions) {
if (!await this.isCertificateExpired(ext)) {
expiredExtensions.push(ext);
}
}
if (expiredExtensions.length === 0) {
this.logger.info(`没有过期的扩展证书,跳过删除`);
return;
}
const query: any = {
ListenerId: listenerId
};
expiredExtensions.forEach((ext, index) => {
const idx = index + 1;
query[`DomainExtensions.${idx}.Action`] = "delete";
query[`DomainExtensions.${idx}.DomainExtensionId`] = ext.DomainExtensionId;
});
this.logger.info(`准备删除过期扩展证书,数量:${expiredExtensions.length}个,query:${JSON.stringify(query)}`);
await service.request({
action: "ModifyListenerAttributes",
query: query
});
this.logger.info(`删除${expiredExtensions.length}个过期扩展证书成功`);
await this.ctx.utils.sleep(5000);
}
private async getCertService(access: VolcengineAccess) {
const client = new VolcengineClient({
@@ -189,6 +288,54 @@ export class VolcengineDeployToALB extends AbstractTaskPlugin {
return service;
}
private async isCertificateExpired(extension: any): Promise<boolean> {
try {
let certificateId: string;
// 根据证书来源获取证书ID
if (extension.CertificateSource === "cert_center") {
certificateId = extension.CertCenterCertificateId;
} else if (extension.CertificateSource === "alb") {
this.logger.warn(`ALB证书不支持过期检查,跳过`);
return false;
} else if (extension.CertificateSource === "pca_leaf") {
this.logger.warn(`PCA Leaf证书不支持过期检查,跳过`);
return false;
} else {
this.logger.warn(`未知的证书来源: ${extension.CertificateSource},跳过`);
return false;
}
if (!certificateId) {
this.logger.warn(`证书ID为空,跳过`);
return false;
}
// 获取证书服务
const access = await this.getAccess<VolcengineAccess>(this.accessId);
const certService = await this.getCertService(access);
// 获取证书详情
const certDetail = await certService.GetCertificateDetail(certificateId);
// 判断证书是否过期
if (certDetail.NotAfter) {
const expireTime = dayjs(certDetail.NotAfter);
const now = dayjs();
const isExpired = expireTime.isBefore(now);
if (isExpired) {
this.logger.info(`证书 ${certificateId} 已过期,过期时间: ${expireTime.toISOString()}`);
}
return isExpired;
}
return false;
} catch (error) {
this.logger.error(`检查证书是否过期失败: ${error.message || error}`);
return false;
}
}
async onGetListenerList(data: any) {
if (!this.accessId) {
throw new Error("请选择Access授权");
@@ -42,6 +42,17 @@ export class VolcengineClient {
});
return res.Result.InstanceId || res.Result.RepeatId;
};
service.GetCertificateDetail = async (certificateId: string) => {
const res = await service.request({
action: "CertificateGetInstance",
method: "POST",
body: {
InstanceId: certificateId
}
});
return res.Result;
};
return service;
}
+1 -1
View File
@@ -1 +1 @@
01:13
00:52
+1 -1
View File
@@ -1 +1 @@
01:30
01:11