mirror of
https://github.com/certd/certd.git
synced 2026-08-03 03:50:15 +08:00
feat: 新增证书申请参数模版管理,开放接口支持使用证书参数模版和指定证书申请参数
This commit is contained in:
@@ -31,7 +31,11 @@ header中传入x-certd-token即可调用开放接口
|
|||||||
支持证书id和域名两种方式获取证书。
|
支持证书id和域名两种方式获取证书。
|
||||||
|
|
||||||
### 创建新的证书申请
|
### 创建新的证书申请
|
||||||
参数autoApply=true,将在没有证书时自动触发申请证书,检查逻辑如下:
|
参数`autoApply=true`将在没有证书时自动触发申请证书。申请参数支持另外传入:
|
||||||
|
- `autoApplyTemplateId`:使用指定 ID 的证书申请参数模版;不传时不使用模版
|
||||||
|
- `autoApplyParams`:自定义证书申请参数,会与系统默认参数、模版参数合并,并覆盖同名字段
|
||||||
|
|
||||||
|
检查逻辑如下:
|
||||||
1. 如果证书仓库里面有,且没有过期,就直接返回证书
|
1. 如果证书仓库里面有,且没有过期,就直接返回证书
|
||||||
2. 如果没有或者已过期,就会去找流水线,有就触发流水线执行
|
2. 如果没有或者已过期,就会去找流水线,有就触发流水线执行
|
||||||
3. 如果没有流水线,就创建一个流水线,触发运行(`注意:需要提前在域名管理中配置好域名校验方式,否则会申请失败`)
|
3. 如果没有流水线,就创建一个流水线,触发运行(`注意:需要提前在域名管理中配置好域名校验方式,否则会申请失败`)
|
||||||
@@ -48,4 +52,4 @@ header中传入x-certd-token即可调用开放接口
|
|||||||
支持自动扫描主机`Nginx`配置,然后从Certd拉取证书并部署。
|
支持自动扫描主机`Nginx`配置,然后从Certd拉取证书并部署。
|
||||||
在不想暴露ssh主机密码情况下,该工具非常好用。
|
在不想暴露ssh主机密码情况下,该工具非常好用。
|
||||||
|
|
||||||
开源地址: https://github.com/Youngxj/SSL-Assistant
|
开源地址: https://github.com/Youngxj/SSL-Assistant
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default {
|
|||||||
settings: "Settings",
|
settings: "Settings",
|
||||||
accessManager: "Access Management",
|
accessManager: "Access Management",
|
||||||
dnsPersistRecord: "DNS Persist Records",
|
dnsPersistRecord: "DNS Persist Records",
|
||||||
|
certApplyTemplate: "Certificate Apply Templates",
|
||||||
subDomain: "Subdomain Delegation Settings",
|
subDomain: "Subdomain Delegation Settings",
|
||||||
pipelineGroup: "Pipeline Group Management",
|
pipelineGroup: "Pipeline Group Management",
|
||||||
openKey: "Open API Key",
|
openKey: "Open API Key",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export default {
|
|||||||
settings: "设置",
|
settings: "设置",
|
||||||
accessManager: "授权管理",
|
accessManager: "授权管理",
|
||||||
dnsPersistRecord: "DNS持久验证记录",
|
dnsPersistRecord: "DNS持久验证记录",
|
||||||
|
certApplyTemplate: "证书申请参数模版",
|
||||||
subDomain: "子域名托管设置",
|
subDomain: "子域名托管设置",
|
||||||
pipelineGroup: "流水线分组管理",
|
pipelineGroup: "流水线分组管理",
|
||||||
openKey: "开放接口密钥",
|
openKey: "开放接口密钥",
|
||||||
|
|||||||
@@ -197,6 +197,17 @@ export const certdResources = [
|
|||||||
keepAlive: true,
|
keepAlive: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "certd.certApplyTemplate",
|
||||||
|
name: "CertApplyTemplate",
|
||||||
|
path: "/certd/cert/apply-template",
|
||||||
|
component: "/certd/cert/apply-template/index.vue",
|
||||||
|
meta: {
|
||||||
|
icon: "ion:list-circle-outline",
|
||||||
|
auth: true,
|
||||||
|
keepAlive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "certd.subDomain",
|
title: "certd.subDomain",
|
||||||
name: "SubDomain",
|
name: "SubDomain",
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { request } from "/src/api/service";
|
||||||
|
|
||||||
|
const apiPrefix = "/cert/apply-template";
|
||||||
|
|
||||||
|
export async function GetList(query: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/page",
|
||||||
|
method: "post",
|
||||||
|
data: query,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ListAll() {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/list",
|
||||||
|
method: "post",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function AddObj(obj: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/add",
|
||||||
|
method: "post",
|
||||||
|
data: obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function UpdateObj(obj: any) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/update",
|
||||||
|
method: "post",
|
||||||
|
data: obj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DelObj(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/delete",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetObj(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/info",
|
||||||
|
method: "post",
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function SetDefault(id: number) {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/setDefault",
|
||||||
|
method: "post",
|
||||||
|
data: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetDefault() {
|
||||||
|
return await request({
|
||||||
|
url: apiPrefix + "/default",
|
||||||
|
method: "post",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
|
||||||
|
import { message } from "ant-design-vue";
|
||||||
|
import * as api from "./api";
|
||||||
|
import { useProjectStore } from "/@/store/project";
|
||||||
|
import { usePluginStore } from "/@/store/plugin";
|
||||||
|
import { buildCertApplyTemplateColumns, buildTemplateSubmitData, pickCertApplyTemplateParams } from "./fields";
|
||||||
|
|
||||||
|
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
|
||||||
|
const pluginStore = usePluginStore();
|
||||||
|
const projectStore = useProjectStore();
|
||||||
|
const { openCrudFormDialog } = useFormWrapper();
|
||||||
|
const isDefaultDict = dict({
|
||||||
|
data: [
|
||||||
|
{ value: true, label: "默认", color: "green" },
|
||||||
|
{ value: false, label: "否", color: "gray" },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
|
||||||
|
return await api.GetList(query);
|
||||||
|
};
|
||||||
|
const addRequest = async ({ form }: AddReq) => {
|
||||||
|
return await api.AddObj(buildTemplateSubmitData(form));
|
||||||
|
};
|
||||||
|
const editRequest = async ({ form, row }: EditReq) => {
|
||||||
|
form.id = row.id;
|
||||||
|
return await api.UpdateObj(buildTemplateSubmitData(form));
|
||||||
|
};
|
||||||
|
const delRequest = async ({ row }: DelReq) => {
|
||||||
|
return await api.DelObj(row.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function setDefault(row: any) {
|
||||||
|
await api.SetDefault(row.id);
|
||||||
|
message.success("设置成功");
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openForm(row?: any) {
|
||||||
|
const certPlugin: any = await pluginStore.getPluginDefine("CertApply");
|
||||||
|
const columns = buildCertApplyTemplateColumns(certPlugin);
|
||||||
|
const content = row?.content ? (typeof row.content === "string" ? JSON.parse(row.content || "{}") : row.content) : {};
|
||||||
|
const initialForm = row
|
||||||
|
? {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isDefault: row.isDefault,
|
||||||
|
disabled: row.disabled,
|
||||||
|
...pickCertApplyTemplateParams(content),
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
await openCrudFormDialog({
|
||||||
|
crudOptions: {
|
||||||
|
columns,
|
||||||
|
form: {
|
||||||
|
mode: row ? "edit" : "add",
|
||||||
|
initialForm,
|
||||||
|
wrapper: {
|
||||||
|
width: 1100,
|
||||||
|
title: row ? "编辑证书申请参数模版" : "新增证书申请参数模版",
|
||||||
|
saveRemind: false,
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
span: 12,
|
||||||
|
},
|
||||||
|
async doSubmit({ form }: any) {
|
||||||
|
if (row) {
|
||||||
|
await editRequest({ form, row } as any);
|
||||||
|
} else {
|
||||||
|
await addRequest({ form } as any);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async afterSubmit() {
|
||||||
|
await crudExpose.doRefresh();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
crudOptions: {
|
||||||
|
request: {
|
||||||
|
pageRequest,
|
||||||
|
addRequest,
|
||||||
|
editRequest,
|
||||||
|
delRequest,
|
||||||
|
},
|
||||||
|
search: {
|
||||||
|
initialForm: {
|
||||||
|
...projectStore.getSearchForm(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actionbar: {
|
||||||
|
buttons: {
|
||||||
|
add: {
|
||||||
|
icon: "ion:add-circle-outline",
|
||||||
|
click: () => openForm(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rowHandle: {
|
||||||
|
fixed: "right",
|
||||||
|
width: 120,
|
||||||
|
buttons: {
|
||||||
|
edit: {
|
||||||
|
click: ({ row }) => openForm(row),
|
||||||
|
},
|
||||||
|
remove: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: {
|
||||||
|
id: {
|
||||||
|
title: "ID",
|
||||||
|
type: "number",
|
||||||
|
column: { width: 80 },
|
||||||
|
form: { show: false },
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
title: "模版名称",
|
||||||
|
type: "text",
|
||||||
|
search: { show: true },
|
||||||
|
column: { minWidth: 220 },
|
||||||
|
},
|
||||||
|
isDefault: {
|
||||||
|
title: "默认",
|
||||||
|
type: "dict-switch",
|
||||||
|
dict: isDefaultDict,
|
||||||
|
column: {
|
||||||
|
width: 150,
|
||||||
|
cellRender({ value, row }) {
|
||||||
|
return (
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<fs-values-format modelValue={value} dict={isDefaultDict}></fs-values-format>
|
||||||
|
{!row.isDefault && (
|
||||||
|
<a-tooltip title="设为默认">
|
||||||
|
<fs-icon class="pointer color-primary" icon="ion:star-outline" onClick={() => setDefault(row)}></fs-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
title: "禁用",
|
||||||
|
type: "dict-switch",
|
||||||
|
column: { width: 100 },
|
||||||
|
},
|
||||||
|
createTime: {
|
||||||
|
title: "创建时间",
|
||||||
|
type: "datetime",
|
||||||
|
column: { width: 180 },
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
title: "配置",
|
||||||
|
type: "text",
|
||||||
|
column: { show: false },
|
||||||
|
form: { show: false },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { cloneDeep, merge, omit } from "lodash-es";
|
||||||
|
import { useReference } from "/@/use/use-refrence";
|
||||||
|
|
||||||
|
export const certApplyTemplateExcludeParamFields = ["domains", "domainsVerifyPlan"];
|
||||||
|
|
||||||
|
const excludeFieldSet = new Set(certApplyTemplateExcludeParamFields);
|
||||||
|
|
||||||
|
export function pickCertApplyTemplateParams(input: any = {}) {
|
||||||
|
const params: any = {};
|
||||||
|
for (const key of Object.keys(input || {})) {
|
||||||
|
if (!excludeFieldSet.has(key) && input[key] !== undefined) {
|
||||||
|
params[key] = input[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCertApplyTemplateColumns(certPlugin: any) {
|
||||||
|
const columns: any = {
|
||||||
|
name: {
|
||||||
|
title: "模版名称",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
required: true,
|
||||||
|
order: -1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const key of Object.keys(certPlugin?.input || {})) {
|
||||||
|
if (excludeFieldSet.has(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const inputDefine = cloneDeep(certPlugin?.input?.[key]);
|
||||||
|
if (!inputDefine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
useReference(inputDefine);
|
||||||
|
columns[key] = {
|
||||||
|
title: inputDefine.title,
|
||||||
|
form: {
|
||||||
|
...inputDefine,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// if (columns.acmeAccountAccessId?.form) {
|
||||||
|
// columns.acmeAccountAccessId.form.show = true;
|
||||||
|
// columns.acmeAccountAccessId.form.required = false;
|
||||||
|
// columns.acmeAccountAccessId.form.component = {
|
||||||
|
// ...columns.acmeAccountAccessId.form.component,
|
||||||
|
// type: "acmeAccount",
|
||||||
|
// subtype: undefined,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
merge(columns, {
|
||||||
|
isDefault: {
|
||||||
|
title: "默认模版",
|
||||||
|
type: "switch",
|
||||||
|
form: {
|
||||||
|
value: false,
|
||||||
|
component: {
|
||||||
|
name: "a-switch",
|
||||||
|
vModel: "checked",
|
||||||
|
},
|
||||||
|
order: 900,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
title: "禁用",
|
||||||
|
type: "switch",
|
||||||
|
form: {
|
||||||
|
value: false,
|
||||||
|
component: {
|
||||||
|
name: "a-switch",
|
||||||
|
vModel: "checked",
|
||||||
|
},
|
||||||
|
order: 901,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTemplateSubmitData(form: any) {
|
||||||
|
return {
|
||||||
|
id: form.id,
|
||||||
|
name: form.name,
|
||||||
|
content: pickCertApplyTemplateParams(omit(form, ["id", "name", "content", "isDefault", "disabled"])),
|
||||||
|
isDefault: form.isDefault,
|
||||||
|
disabled: form.disabled,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<fs-page class="page-cert-apply-template">
|
||||||
|
<template #header>
|
||||||
|
<div class="title">
|
||||||
|
证书申请参数模版
|
||||||
|
<span class="sub">预设证书申请参数,不包含域名和校验方式</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
|
||||||
|
</fs-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { onActivated, onMounted } from "vue";
|
||||||
|
import { useFs } from "@fast-crud/fast-crud";
|
||||||
|
import createCrudOptions from "./crud";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CertApplyTemplate",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { crudBinding, crudRef, crudExpose } = useFs({
|
||||||
|
createCrudOptions,
|
||||||
|
context: {
|
||||||
|
permission: { isProjectPermission: true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
onActivated(() => {
|
||||||
|
crudExpose.doRefresh();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { checkPipelineLimit } from "/@/views/certd/pipeline/utils";
|
import { checkPipelineLimit } from "/@/views/certd/pipeline/utils";
|
||||||
import { cloneDeep, merge, omit } from "lodash-es";
|
import { cloneDeep, merge, omit } from "lodash-es";
|
||||||
import { message } from "ant-design-vue";
|
import { message, Modal } from "ant-design-vue";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud";
|
import { compute, CreateCrudOptionsRet, dict, useFormWrapper } from "@fast-crud/fast-crud";
|
||||||
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
|
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
|
||||||
import { useReference } from "/@/use/use-refrence";
|
import { useReference } from "/@/use/use-refrence";
|
||||||
import { computed, provide, Ref, ref } from "vue";
|
import { computed, provide, reactive, Ref, ref } from "vue";
|
||||||
import * as api from "../api";
|
import * as api from "../api";
|
||||||
import { PluginGroup, usePluginStore } from "/@/store/plugin";
|
import { PluginGroup, usePluginStore } from "/@/store/plugin";
|
||||||
import { createNotificationApi } from "/@/views/certd/notification/api";
|
import { createNotificationApi } from "/@/views/certd/notification/api";
|
||||||
@@ -14,6 +14,8 @@ import GroupSelector from "../group/group-selector.vue";
|
|||||||
import { useI18n } from "/src/locales";
|
import { useI18n } from "/src/locales";
|
||||||
import { useSettingStore } from "/@/store/settings";
|
import { useSettingStore } from "/@/store/settings";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import * as certApplyTemplateApi from "/@/views/certd/cert/apply-template/api";
|
||||||
|
import { buildCertApplyTemplateColumns, buildTemplateSubmitData, pickCertApplyTemplateParams } from "/@/views/certd/cert/apply-template/fields";
|
||||||
|
|
||||||
export function fillPipelineByDefaultForm(pipeline: any, form: any) {
|
export function fillPipelineByDefaultForm(pipeline: any, form: any) {
|
||||||
const triggers = [];
|
const triggers = [];
|
||||||
@@ -107,6 +109,7 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
|
|||||||
const pluginStore = usePluginStore();
|
const pluginStore = usePluginStore();
|
||||||
const settingStore = useSettingStore();
|
const settingStore = useSettingStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { openCrudFormDialog: openInnerCrudFormDialog } = useFormWrapper();
|
||||||
|
|
||||||
function createCrudOptions(req: { certPlugin: any; doSubmit: any; title?: string; initialForm?: any }): CreateCrudOptionsRet {
|
function createCrudOptions(req: { certPlugin: any; doSubmit: any; title?: string; initialForm?: any }): CreateCrudOptionsRet {
|
||||||
const inputs: any = {};
|
const inputs: any = {};
|
||||||
@@ -150,6 +153,236 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
|
|||||||
|
|
||||||
const initialForm = req.initialForm || {};
|
const initialForm = req.initialForm || {};
|
||||||
initialForm.type = certPlugin.name;
|
initialForm.type = certPlugin.name;
|
||||||
|
const templateDict = dict({
|
||||||
|
value: "id",
|
||||||
|
label: "name",
|
||||||
|
async getData() {
|
||||||
|
return await certApplyTemplateApi.ListAll();
|
||||||
|
},
|
||||||
|
async getNodesByValues(ids: any[]) {
|
||||||
|
const list = await certApplyTemplateApi.ListAll();
|
||||||
|
return list.filter((item: any) => ids.includes(item.id));
|
||||||
|
},
|
||||||
|
immediate: false,
|
||||||
|
});
|
||||||
|
const applyTemplates = reactive<any[]>([]);
|
||||||
|
|
||||||
|
async function reloadApplyTemplates() {
|
||||||
|
const list = await certApplyTemplateApi.ListAll();
|
||||||
|
applyTemplates.splice(0, applyTemplates.length, ...list);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyTemplateToForm(templateId: number, form: any) {
|
||||||
|
if (!templateId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const template = await certApplyTemplateApi.GetObj(templateId);
|
||||||
|
const params = pickCertApplyTemplateParams(typeof template.content === "string" ? JSON.parse(template.content || "{}") : template.content);
|
||||||
|
form.input = {
|
||||||
|
...form.input,
|
||||||
|
...params,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectedApplyTemplateName(form: any) {
|
||||||
|
if (!form?.applyTemplateId) {
|
||||||
|
return "选择模版";
|
||||||
|
}
|
||||||
|
const template = applyTemplates.find(item => item.id === form.applyTemplateId);
|
||||||
|
return template?.name || "选择模版";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveCurrentTemplate(form: any) {
|
||||||
|
await openInnerCrudFormDialog({
|
||||||
|
crudOptions: {
|
||||||
|
columns: {
|
||||||
|
name: {
|
||||||
|
title: "模版名称",
|
||||||
|
type: "text",
|
||||||
|
form: {
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isDefault: {
|
||||||
|
title: "设为默认",
|
||||||
|
type: "switch",
|
||||||
|
form: {
|
||||||
|
value: false,
|
||||||
|
component: {
|
||||||
|
name: "a-switch",
|
||||||
|
vModel: "checked",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
mode: "add",
|
||||||
|
wrapper: {
|
||||||
|
width: 520,
|
||||||
|
title: "保存证书申请参数模版",
|
||||||
|
saveRemind: false,
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
async doSubmit({ form: templateForm }: any) {
|
||||||
|
await certApplyTemplateApi.AddObj({
|
||||||
|
name: templateForm.name,
|
||||||
|
isDefault: templateForm.isDefault,
|
||||||
|
content: pickCertApplyTemplateParams(form.input),
|
||||||
|
});
|
||||||
|
await reloadApplyTemplates();
|
||||||
|
await templateDict.reloadDict();
|
||||||
|
message.success("保存成功");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openApplyTemplateEditor(templateId: number) {
|
||||||
|
const row = await certApplyTemplateApi.GetObj(templateId);
|
||||||
|
const columns = buildCertApplyTemplateColumns(certPlugin);
|
||||||
|
const content = row?.content ? (typeof row.content === "string" ? JSON.parse(row.content || "{}") : row.content) : {};
|
||||||
|
await openInnerCrudFormDialog({
|
||||||
|
crudOptions: {
|
||||||
|
columns,
|
||||||
|
form: {
|
||||||
|
mode: "edit",
|
||||||
|
initialForm: {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
isDefault: row.isDefault,
|
||||||
|
disabled: row.disabled,
|
||||||
|
...pickCertApplyTemplateParams(content),
|
||||||
|
},
|
||||||
|
wrapper: {
|
||||||
|
width: 1100,
|
||||||
|
title: "编辑证书申请参数模版",
|
||||||
|
saveRemind: false,
|
||||||
|
},
|
||||||
|
col: {
|
||||||
|
span: 12,
|
||||||
|
},
|
||||||
|
async doSubmit({ form: templateForm }: any) {
|
||||||
|
await certApplyTemplateApi.UpdateObj(buildTemplateSubmitData(templateForm));
|
||||||
|
await reloadApplyTemplates();
|
||||||
|
await templateDict.reloadDict();
|
||||||
|
message.success("保存成功");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteApplyTemplate(templateId: number) {
|
||||||
|
Modal.confirm({
|
||||||
|
title: "确认删除该模版?",
|
||||||
|
content: "删除后无法恢复。",
|
||||||
|
async onOk() {
|
||||||
|
await certApplyTemplateApi.DelObj(templateId);
|
||||||
|
await reloadApplyTemplates();
|
||||||
|
await templateDict.reloadDict();
|
||||||
|
message.success("删除成功");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopMenuAction(event: MouseEvent, action: () => void) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
action();
|
||||||
|
}
|
||||||
|
|
||||||
|
function goApplyTemplateManage() {
|
||||||
|
formWrapperRef.value?.close?.();
|
||||||
|
router.push({ name: "CertApplyTemplate" });
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTemplateFooter(scope: any) {
|
||||||
|
if (certPlugin.name !== "CertApply") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const form = scope?.getFormData?.();
|
||||||
|
if (!form) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div class="flex items-center">
|
||||||
|
<a-dropdown
|
||||||
|
trigger={["click"]}
|
||||||
|
onOpenChange={(open: boolean) => {
|
||||||
|
if (open) {
|
||||||
|
reloadApplyTemplates();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
v-slots={{
|
||||||
|
overlay: () => (
|
||||||
|
<a-menu
|
||||||
|
onClick={({ key }: any) => {
|
||||||
|
if (key === "save") {
|
||||||
|
saveCurrentTemplate(form);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key === "empty") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const templateId = Number(key);
|
||||||
|
form.applyTemplateId = templateId;
|
||||||
|
applyTemplateToForm(templateId, form);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{applyTemplates.length === 0 ? (
|
||||||
|
<a-menu-item key="empty" disabled>
|
||||||
|
暂无模版
|
||||||
|
</a-menu-item>
|
||||||
|
) : (
|
||||||
|
applyTemplates.map(item => (
|
||||||
|
<a-menu-item key={item.id}>
|
||||||
|
<div class="flex items-center justify-between gap-4 min-w-80">
|
||||||
|
<span class="truncate">{item.name}</span>
|
||||||
|
<span class="flex items-center gap-2 shrink-0">
|
||||||
|
<a-button size="small" type="link" onClick={(event: MouseEvent) => stopMenuAction(event, () => openApplyTemplateEditor(item.id))}>
|
||||||
|
编辑
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" type="link" danger onClick={(event: MouseEvent) => stopMenuAction(event, () => deleteApplyTemplate(item.id))}>
|
||||||
|
删除
|
||||||
|
</a-button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a-menu-item>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<a-menu-divider />
|
||||||
|
<a-menu-item key="save">
|
||||||
|
<div class="flex items-center justify-between gap-4 min-w-80">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<fs-icon icon="ion:save-outline" />
|
||||||
|
<span class="ml-1">保存当前参数为模版</span>
|
||||||
|
</div>
|
||||||
|
<a-tooltip title="证书参数模版管理">
|
||||||
|
<a-button size="small" type="link" onClick={(event: MouseEvent) => stopMenuAction(event, goApplyTemplateManage)}>
|
||||||
|
<fs-icon icon="ion:list-circle-outline" />
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<a-tooltip title="选择参数模版,自动填充证书申请参数">
|
||||||
|
<a-button>
|
||||||
|
<span class="inline-block max-w-48 truncate align-bottom">{getSelectedApplyTemplateName(form)}</span>
|
||||||
|
<fs-icon icon="ion:chevron-down" class="ml-1" />
|
||||||
|
</a-button>
|
||||||
|
</a-tooltip>
|
||||||
|
</a-dropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
crudOptions: {
|
crudOptions: {
|
||||||
form: {
|
form: {
|
||||||
@@ -160,6 +393,9 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
|
|||||||
width: 1350,
|
width: 1350,
|
||||||
saveRemind: false,
|
saveRemind: false,
|
||||||
title: req.title || t("certd.pipelineForm.createTitle"),
|
title: req.title || t("certd.pipelineForm.createTitle"),
|
||||||
|
slots: {
|
||||||
|
"form-footer-left": renderTemplateFooter,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
group: {
|
group: {
|
||||||
groups: {
|
groups: {
|
||||||
@@ -323,6 +559,15 @@ export function useCertPipelineCreator({ formWrapperRef }: { formWrapperRef: Ref
|
|||||||
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
|
initialForm.input[key] = pluginSysConfig.sysSetting?.input[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const defaultTemplate = req.pluginName === "CertApply" ? await certApplyTemplateApi.GetDefault() : null;
|
||||||
|
if (defaultTemplate) {
|
||||||
|
initialForm.applyTemplateId = defaultTemplate.id;
|
||||||
|
const templateParams = pickCertApplyTemplateParams(typeof defaultTemplate.content === "string" ? JSON.parse(defaultTemplate.content || "{}") : defaultTemplate.content);
|
||||||
|
initialForm.input = {
|
||||||
|
...initialForm.input,
|
||||||
|
...templateParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function doSubmit({ form }: any) {
|
async function doSubmit({ form }: any) {
|
||||||
// const certDetail = readCertDetail(form.cert.crt);
|
// const certDetail = readCertDetail(form.cert.crt);
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ import { groupDictRef } from "./group/dicts";
|
|||||||
import { useCertPipelineCreator } from "./certd-form/use";
|
import { useCertPipelineCreator } from "./certd-form/use";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import { useCrudPermission } from "/@/plugin/permission";
|
import { useCrudPermission } from "/@/plugin/permission";
|
||||||
import CertdForm from "./certd-form/certd-form.vue";
|
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PipelineManager",
|
name: "PipelineManager",
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE "cd_cert_apply_template"
|
||||||
|
(
|
||||||
|
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
"user_id" integer NOT NULL,
|
||||||
|
"project_id" integer NULL,
|
||||||
|
"name" varchar(100) NOT NULL,
|
||||||
|
"content" text NOT NULL,
|
||||||
|
"is_default" boolean NOT NULL DEFAULT (false),
|
||||||
|
"disabled" boolean NOT NULL DEFAULT (false),
|
||||||
|
"create_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||||
|
"update_time" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX "index_cert_apply_template_user_id" ON "cd_cert_apply_template" ("user_id");
|
||||||
|
CREATE INDEX "index_cert_apply_template_project_id" ON "cd_cert_apply_template" ("project_id");
|
||||||
|
CREATE INDEX "index_cert_apply_template_default" ON "cd_cert_apply_template" ("user_id", "project_id", "is_default");
|
||||||
@@ -134,5 +134,7 @@ export class MainConfiguration {
|
|||||||
});
|
});
|
||||||
|
|
||||||
logger.info("当前环境:", this.app.getEnv()); // prod
|
logger.info("当前环境:", this.app.getEnv()); // prod
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ export type CertGetReq = {
|
|||||||
domains?: string;
|
domains?: string;
|
||||||
certId: number;
|
certId: number;
|
||||||
autoApply?: boolean;
|
autoApply?: boolean;
|
||||||
|
autoApplyTemplateId?: number;
|
||||||
|
autoApplyParams?: Record<string, any> | string;
|
||||||
format?: string; //默认是所有,pem,der,p12,pfx,jks,one,p7b
|
format?: string; //默认是所有,pem,der,p12,pfx,jks,one,p7b
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -43,6 +45,8 @@ export class OpenCertController extends BaseOpenController {
|
|||||||
domains: req.domains,
|
domains: req.domains,
|
||||||
certId: req.certId,
|
certId: req.certId,
|
||||||
autoApply: req.autoApply ?? false,
|
autoApply: req.autoApply ?? false,
|
||||||
|
autoApplyTemplateId: req.autoApplyTemplateId,
|
||||||
|
autoApplyParams: typeof req.autoApplyParams === "string" ? JSON.parse(req.autoApplyParams) : req.autoApplyParams,
|
||||||
format: req.format,
|
format: req.format,
|
||||||
projectId,
|
projectId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { ALL, Body, Controller, Inject, Post, Provide, Query } from "@midwayjs/core";
|
||||||
|
import { Constants, CrudController } from "@certd/lib-server";
|
||||||
|
import { ApiTags } from "@midwayjs/swagger";
|
||||||
|
import { CertApplyTemplateService } from "../../../modules/cert/service/cert-apply-template-service.js";
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Controller("/api/cert/apply-template")
|
||||||
|
@ApiTags(["cert"])
|
||||||
|
export class CertApplyTemplateController extends CrudController<CertApplyTemplateService> {
|
||||||
|
@Inject()
|
||||||
|
service: CertApplyTemplateService;
|
||||||
|
|
||||||
|
getService(): CertApplyTemplateService {
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/page", { description: Constants.per.authOnly, summary: "查询证书申请参数模版分页列表" })
|
||||||
|
async page(@Body(ALL) body: any) {
|
||||||
|
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||||
|
body.query = body.query ?? {};
|
||||||
|
body.query.projectId = projectId;
|
||||||
|
body.query.userId = userId;
|
||||||
|
return super.page(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/list", { description: Constants.per.authOnly, summary: "查询证书申请参数模版列表" })
|
||||||
|
async list(@Body(ALL) body: any) {
|
||||||
|
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||||
|
body.query = body.query ?? {};
|
||||||
|
body.query.projectId = projectId;
|
||||||
|
body.query.userId = userId;
|
||||||
|
body.query.disabled = false;
|
||||||
|
return super.list(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/add", { description: Constants.per.authOnly, summary: "添加证书申请参数模版" })
|
||||||
|
async add(@Body(ALL) bean: any) {
|
||||||
|
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||||
|
bean.projectId = projectId;
|
||||||
|
bean.userId = userId;
|
||||||
|
return super.add(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/update", { description: Constants.per.authOnly, summary: "更新证书申请参数模版" })
|
||||||
|
async update(@Body(ALL) bean: any) {
|
||||||
|
await this.checkOwner(this.getService(), bean.id, "write");
|
||||||
|
delete bean.userId;
|
||||||
|
delete bean.projectId;
|
||||||
|
return super.update(bean);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/info", { description: Constants.per.authOnly, summary: "查询证书申请参数模版详情" })
|
||||||
|
async info(@Query("id") id: number) {
|
||||||
|
await this.checkOwner(this.getService(), id, "read");
|
||||||
|
return super.info(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/delete", { description: Constants.per.authOnly, summary: "删除证书申请参数模版" })
|
||||||
|
async delete(@Query("id") id: number) {
|
||||||
|
await this.checkOwner(this.getService(), id, "write");
|
||||||
|
return super.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/setDefault", { description: Constants.per.authOnly, summary: "设置默认证书申请参数模版" })
|
||||||
|
async setDefault(@Body("id") id: number) {
|
||||||
|
const { projectId, userId } = await this.getProjectUserIdWrite();
|
||||||
|
return this.ok(await this.service.setDefault(id, userId, projectId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post("/default", { description: Constants.per.authOnly, summary: "查询默认证书申请参数模版" })
|
||||||
|
async getDefault() {
|
||||||
|
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||||
|
return this.ok(await this.service.getDefault(userId, projectId));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 证书申请参数模版
|
||||||
|
*/
|
||||||
|
@Entity("cd_cert_apply_template")
|
||||||
|
export class CertApplyTemplateEntity {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ comment: "用户ID", name: "user_id" })
|
||||||
|
userId: number;
|
||||||
|
|
||||||
|
@Column({ name: "project_id", comment: "项目ID" })
|
||||||
|
projectId: number;
|
||||||
|
|
||||||
|
@Column({ comment: "模版名称", length: 100 })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ comment: "配置", type: "text" })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Column({ name: "is_default", comment: "是否默认模版", default: false })
|
||||||
|
isDefault: boolean;
|
||||||
|
|
||||||
|
@Column({ comment: "是否禁用", default: false })
|
||||||
|
disabled: boolean;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
comment: "创建时间",
|
||||||
|
name: "create_time",
|
||||||
|
default: () => "CURRENT_TIMESTAMP",
|
||||||
|
})
|
||||||
|
createTime: Date;
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
comment: "修改时间",
|
||||||
|
name: "update_time",
|
||||||
|
default: () => "CURRENT_TIMESTAMP",
|
||||||
|
})
|
||||||
|
updateTime: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { pickCertApplyCustomParams, pickCertApplyTemplateParams } from "./cert-apply-template-fields.js";
|
||||||
|
|
||||||
|
describe("cert apply template fields", () => {
|
||||||
|
it("keeps certificate apply and domain verify params but drops domains and verify plan for template", () => {
|
||||||
|
const params = pickCertApplyTemplateParams({
|
||||||
|
domains: ["example.com"],
|
||||||
|
challengeType: "dns",
|
||||||
|
dnsProviderType: "aliyun",
|
||||||
|
dnsProviderAccess: 1,
|
||||||
|
dnsProviderAccessType: "aliyun",
|
||||||
|
domainsVerifyPlan: [{ domain: "example.com", type: "dns" }],
|
||||||
|
sslProvider: "google",
|
||||||
|
acmeAccountAccessId: 2,
|
||||||
|
privateKeyType: "ec_256",
|
||||||
|
pfxPassword: "secret",
|
||||||
|
renewDays: 15,
|
||||||
|
preferredChain: "GTS Root R1",
|
||||||
|
newApplyParam: "kept",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(params, {
|
||||||
|
challengeType: "dns",
|
||||||
|
dnsProviderType: "aliyun",
|
||||||
|
dnsProviderAccess: 1,
|
||||||
|
dnsProviderAccessType: "aliyun",
|
||||||
|
sslProvider: "google",
|
||||||
|
acmeAccountAccessId: 2,
|
||||||
|
privateKeyType: "ec_256",
|
||||||
|
pfxPassword: "secret",
|
||||||
|
renewDays: 15,
|
||||||
|
preferredChain: "GTS Root R1",
|
||||||
|
newApplyParam: "kept",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps domain verify plan for custom auto apply params", () => {
|
||||||
|
const params = pickCertApplyCustomParams({
|
||||||
|
domains: ["example.com"],
|
||||||
|
domainsVerifyPlan: [{ domain: "example.com", type: "dns" }],
|
||||||
|
challengeType: "dns",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(params, {
|
||||||
|
domainsVerifyPlan: [{ domain: "example.com", type: "dns" }],
|
||||||
|
challengeType: "dns",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
export type CertApplyTemplateParams = Record<string, any>;
|
||||||
|
|
||||||
|
export const certApplyTemplateExcludeParamFields = ["domains", "domainsVerifyPlan"] as const;
|
||||||
|
export const certApplyCustomExcludeParamFields = ["domains"] as const;
|
||||||
|
|
||||||
|
const certApplyTemplateExcludeParamFieldSet = new Set<string>(certApplyTemplateExcludeParamFields);
|
||||||
|
const certApplyCustomExcludeParamFieldSet = new Set<string>(certApplyCustomExcludeParamFields);
|
||||||
|
|
||||||
|
export function pickCertApplyTemplateParams(input: CertApplyTemplateParams = {}) {
|
||||||
|
return pickCertApplyParams(input, certApplyTemplateExcludeParamFieldSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickCertApplyCustomParams(input: CertApplyTemplateParams = {}) {
|
||||||
|
return pickCertApplyParams(input, certApplyCustomExcludeParamFieldSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickCertApplyParams(input: CertApplyTemplateParams = {}, excludeFieldSet: Set<string>) {
|
||||||
|
const params: CertApplyTemplateParams = {};
|
||||||
|
for (const key of Object.keys(input)) {
|
||||||
|
if (!excludeFieldSet.has(key) && input[key] !== undefined) {
|
||||||
|
params[key] = input[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { CertApplyTemplateService } from "./cert-apply-template-service.js";
|
||||||
|
|
||||||
|
function createService(list: any[]) {
|
||||||
|
const service = new CertApplyTemplateService();
|
||||||
|
(service as any).repository = {
|
||||||
|
async findOne({ where }: any) {
|
||||||
|
return list.find(item => {
|
||||||
|
if (where.id != null && item.id !== where.id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (where.userId != null && item.userId !== where.userId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (where.projectId != null && item.projectId !== where.projectId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (where.isDefault != null && item.isDefault !== where.isDefault) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("CertApplyTemplateService", () => {
|
||||||
|
it("does not apply default template when template id is not specified", async () => {
|
||||||
|
const service = createService([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
isDefault: true,
|
||||||
|
content: JSON.stringify({
|
||||||
|
sslProvider: "google",
|
||||||
|
privateKeyType: "ec_256",
|
||||||
|
renewDays: 10,
|
||||||
|
domains: ["bad.example.com"],
|
||||||
|
challengeType: "dns",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const params = await service.resolveApplyParams({
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(params, {});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses selected template when auto apply uses integer template id", async () => {
|
||||||
|
const service = createService([
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
isDefault: false,
|
||||||
|
content: JSON.stringify({
|
||||||
|
sslProvider: "zerossl",
|
||||||
|
acmeAccountAccessId: 8,
|
||||||
|
preferredChain: "ZeroSSL RSA Domain Secure Site CA",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const params = await service.resolveApplyParams({
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
templateId: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(params, {
|
||||||
|
sslProvider: "zerossl",
|
||||||
|
acmeAccountAccessId: 8,
|
||||||
|
preferredChain: "ZeroSSL RSA Domain Secure Site CA",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses custom params only when template id is not specified", async () => {
|
||||||
|
const service = createService([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
isDefault: true,
|
||||||
|
content: JSON.stringify({
|
||||||
|
sslProvider: "google",
|
||||||
|
renewDays: 10,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const params = await service.resolveApplyParams({
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
params: {
|
||||||
|
renewDays: 30,
|
||||||
|
privateKeyType: "rsa_4096",
|
||||||
|
dnsProviderType: "cloudflare",
|
||||||
|
domainsVerifyPlan: [{ domain: "example.com", type: "dns" }],
|
||||||
|
domains: ["example.com"],
|
||||||
|
challengeType: "auto",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(params, {
|
||||||
|
renewDays: 30,
|
||||||
|
privateKeyType: "rsa_4096",
|
||||||
|
dnsProviderType: "cloudflare",
|
||||||
|
challengeType: "auto",
|
||||||
|
domainsVerifyPlan: [{ domain: "example.com", type: "dns" }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("merges selected template and custom params when both are specified", async () => {
|
||||||
|
const service = createService([
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
isDefault: false,
|
||||||
|
content: JSON.stringify({
|
||||||
|
sslProvider: "zerossl",
|
||||||
|
acmeAccountAccessId: 8,
|
||||||
|
preferredChain: "ZeroSSL RSA Domain Secure Site CA",
|
||||||
|
renewDays: 10,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const params = await service.resolveApplyParams({
|
||||||
|
userId: 10,
|
||||||
|
projectId: 20,
|
||||||
|
templateId: 2,
|
||||||
|
params: {
|
||||||
|
renewDays: 30,
|
||||||
|
privateKeyType: "rsa_4096",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(params, {
|
||||||
|
sslProvider: "zerossl",
|
||||||
|
acmeAccountAccessId: 8,
|
||||||
|
preferredChain: "ZeroSSL RSA Domain Secure Site CA",
|
||||||
|
renewDays: 30,
|
||||||
|
privateKeyType: "rsa_4096",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||||
|
import { InjectEntityModel } from "@midwayjs/typeorm";
|
||||||
|
import { BaseService, ValidateException } from "@certd/lib-server";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { CertApplyTemplateEntity } from "../entity/cert-apply-template.js";
|
||||||
|
import { CertApplyTemplateParams, pickCertApplyCustomParams, pickCertApplyTemplateParams } from "./cert-apply-template-fields.js";
|
||||||
|
|
||||||
|
export type ResolveApplyTemplateReq = {
|
||||||
|
userId: number;
|
||||||
|
projectId?: number;
|
||||||
|
templateId?: number;
|
||||||
|
params?: CertApplyTemplateParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
@Provide()
|
||||||
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
|
export class CertApplyTemplateService extends BaseService<CertApplyTemplateEntity> {
|
||||||
|
@InjectEntityModel(CertApplyTemplateEntity)
|
||||||
|
repository: Repository<CertApplyTemplateEntity>;
|
||||||
|
|
||||||
|
getRepository(): Repository<CertApplyTemplateEntity> {
|
||||||
|
return this.repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
async add(param: any) {
|
||||||
|
param.content = this.stringifyContent(param.content);
|
||||||
|
const res = await super.add(param);
|
||||||
|
if (param.isDefault) {
|
||||||
|
await this.setDefault(res.id, param.userId, param.projectId);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(param: any) {
|
||||||
|
if (param.content != null) {
|
||||||
|
param.content = this.stringifyContent(param.content);
|
||||||
|
}
|
||||||
|
await super.update(param);
|
||||||
|
if (param.isDefault === true) {
|
||||||
|
const entity = await this.info(param.id);
|
||||||
|
await this.setDefault(param.id, entity.userId, entity.projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefault(id: number, userId: number, projectId?: number) {
|
||||||
|
const entity = await this.getTemplateById(id, userId, projectId);
|
||||||
|
if (entity.disabled) {
|
||||||
|
throw new ValidateException("禁用的模版不能设为默认");
|
||||||
|
}
|
||||||
|
await this.repository.update({ userId, projectId }, { isDefault: false });
|
||||||
|
await this.repository.update({ id: entity.id, userId, projectId }, { isDefault: true });
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDefault(userId: number, projectId?: number) {
|
||||||
|
return await this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
isDefault: true,
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async resolveApplyParams(req: ResolveApplyTemplateReq) {
|
||||||
|
const templateParams = await this.getTemplateParams(req);
|
||||||
|
const customParams = pickCertApplyCustomParams(req.params || {});
|
||||||
|
return {
|
||||||
|
...templateParams,
|
||||||
|
...customParams,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTemplateParams(req: ResolveApplyTemplateReq) {
|
||||||
|
if (!req.templateId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const template = await this.getTemplateById(req.templateId, req.userId, req.projectId);
|
||||||
|
if (!template) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return this.parseContent(template.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTemplateById(id: number, userId: number, projectId?: number) {
|
||||||
|
const template = await this.repository.findOne({
|
||||||
|
where: {
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!template) {
|
||||||
|
throw new ValidateException("证书申请参数模版不存在");
|
||||||
|
}
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
private stringifyContent(content: any) {
|
||||||
|
const params = this.parseContent(content);
|
||||||
|
return JSON.stringify(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseContent(content: any) {
|
||||||
|
if (!content) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const raw = typeof content === "string" ? JSON.parse(content) : content;
|
||||||
|
return pickCertApplyTemplateParams(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
|
|||||||
import { CertInfoService } from "../service/cert-info-service.js";
|
import { CertInfoService } from "../service/cert-info-service.js";
|
||||||
import { DomainService } from "../../cert/service/domain-service.js";
|
import { DomainService } from "../../cert/service/domain-service.js";
|
||||||
import { DomainVerifierGetter } from "../../pipeline/service/getter/domain-verifier-getter.js";
|
import { DomainVerifierGetter } from "../../pipeline/service/getter/domain-verifier-getter.js";
|
||||||
|
import { CertApplyTemplateService } from "../../cert/service/cert-apply-template-service.js";
|
||||||
|
import { CertApplyTemplateParams } from "../../cert/service/cert-apply-template-fields.js";
|
||||||
|
|
||||||
@Provide("CertInfoFacade")
|
@Provide("CertInfoFacade")
|
||||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||||
@@ -25,7 +27,10 @@ export class CertInfoFacade {
|
|||||||
@Inject()
|
@Inject()
|
||||||
userSettingsService: UserSettingsService;
|
userSettingsService: UserSettingsService;
|
||||||
|
|
||||||
async getCertInfo(req: { domains?: string; certId?: number; userId: number; projectId: number; autoApply?: boolean; format?: string }) {
|
@Inject()
|
||||||
|
certApplyTemplateService: CertApplyTemplateService;
|
||||||
|
|
||||||
|
async getCertInfo(req: { domains?: string; certId?: number; userId: number; projectId: number; autoApply?: boolean; format?: string; autoApplyTemplateId?: number; autoApplyParams?: CertApplyTemplateParams }) {
|
||||||
const { domains, certId, userId, projectId } = req;
|
const { domains, certId, userId, projectId } = req;
|
||||||
if (certId) {
|
if (certId) {
|
||||||
return await this.certInfoService.getCertInfoById({ id: certId, userId, projectId });
|
return await this.certInfoService.getCertInfoById({ id: certId, userId, projectId });
|
||||||
@@ -43,7 +48,13 @@ export class CertInfoFacade {
|
|||||||
if (matchedList.length === 0) {
|
if (matchedList.length === 0) {
|
||||||
if (req.autoApply === true) {
|
if (req.autoApply === true) {
|
||||||
//自动申请,先创建自动申请流水线
|
//自动申请,先创建自动申请流水线
|
||||||
const pipeline: PipelineEntity = await this.createAutoPipeline({ domains: domainArr, userId, projectId });
|
const pipeline: PipelineEntity = await this.createAutoPipeline({
|
||||||
|
domains: domainArr,
|
||||||
|
userId,
|
||||||
|
projectId,
|
||||||
|
autoApplyTemplateId: req.autoApplyTemplateId,
|
||||||
|
autoApplyParams: req.autoApplyParams,
|
||||||
|
});
|
||||||
await this.triggerApplyPipeline({ pipelineId: pipeline.id });
|
await this.triggerApplyPipeline({ pipelineId: pipeline.id });
|
||||||
} else {
|
} else {
|
||||||
throw new CodeException({
|
throw new CodeException({
|
||||||
@@ -98,7 +109,7 @@ export class CertInfoFacade {
|
|||||||
return matched;
|
return matched;
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAutoPipeline(req: { domains: string[]; userId: number; projectId: number }) {
|
async createAutoPipeline(req: { domains: string[]; userId: number; projectId: number; autoApplyTemplateId?: number; autoApplyParams?: CertApplyTemplateParams }) {
|
||||||
const verifierGetter = new DomainVerifierGetter(req.userId, req.projectId, this.domainService);
|
const verifierGetter = new DomainVerifierGetter(req.userId, req.projectId, this.domainService);
|
||||||
|
|
||||||
const allDomains = [];
|
const allDomains = [];
|
||||||
@@ -123,6 +134,12 @@ export class CertInfoFacade {
|
|||||||
throw new CodeException(Constants.res.openEmailNotFound);
|
throw new CodeException(Constants.res.openEmailNotFound);
|
||||||
}
|
}
|
||||||
const email = userEmailSetting.list[0];
|
const email = userEmailSetting.list[0];
|
||||||
|
const applyParams = await this.certApplyTemplateService.resolveApplyParams({
|
||||||
|
userId: req.userId,
|
||||||
|
projectId: req.projectId,
|
||||||
|
templateId: req.autoApplyTemplateId,
|
||||||
|
params: req.autoApplyParams,
|
||||||
|
});
|
||||||
|
|
||||||
return await this.pipelineService.createAutoPipeline({
|
return await this.pipelineService.createAutoPipeline({
|
||||||
domains: req.domains,
|
domains: req.domains,
|
||||||
@@ -130,6 +147,7 @@ export class CertInfoFacade {
|
|||||||
projectId: req.projectId,
|
projectId: req.projectId,
|
||||||
userId: req.userId,
|
userId: req.userId,
|
||||||
from: "OpenAPI",
|
from: "OpenAPI",
|
||||||
|
applyParams,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import parser from "cron-parser";
|
|||||||
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
import { ProjectService } from "../../sys/enterprise/service/project-service.js";
|
||||||
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
|
import { CertApplyStepInputPatch, updateCertApplyStepInputs } from "./pipeline-batch-update.js";
|
||||||
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
|
import { calcNextSuiteCountUsed } from "./pipeline-suite-limit.js";
|
||||||
|
import { CertApplyTemplateParams } from "../../cert/service/cert-apply-template-fields.js";
|
||||||
const runningTasks: Map<string | number, Executor> = new Map();
|
const runningTasks: Map<string | number, Executor> = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1298,7 +1299,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAutoPipeline(req: { domains: string[]; email: string; userId: number; projectId?: number; from: string }) {
|
async createAutoPipeline(req: { domains: string[]; email: string; userId: number; projectId?: number; from: string; applyParams?: CertApplyTemplateParams }) {
|
||||||
const randomHour = Math.floor(Math.random() * 6);
|
const randomHour = Math.floor(Math.random() * 6);
|
||||||
const randomMin = Math.floor(Math.random() * 60);
|
const randomMin = Math.floor(Math.random() * 60);
|
||||||
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
|
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
|
||||||
@@ -1343,9 +1344,6 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
runnableType: "step",
|
runnableType: "step",
|
||||||
input: {
|
input: {
|
||||||
renewDays: 20,
|
renewDays: 20,
|
||||||
domains: req.domains,
|
|
||||||
email: req.email,
|
|
||||||
challengeType: "auto",
|
|
||||||
sslProvider: "letsencrypt",
|
sslProvider: "letsencrypt",
|
||||||
privateKeyType: "rsa_2048",
|
privateKeyType: "rsa_2048",
|
||||||
certProfile: "classic",
|
certProfile: "classic",
|
||||||
@@ -1356,6 +1354,10 @@ export class PipelineService extends BaseService<PipelineEntity> {
|
|||||||
waitDnsDiffuseTime: 30,
|
waitDnsDiffuseTime: 30,
|
||||||
pfxArgs: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
|
pfxArgs: "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
|
||||||
successNotify: true,
|
successNotify: true,
|
||||||
|
...req.applyParams,
|
||||||
|
domains: req.domains,
|
||||||
|
email: req.email,
|
||||||
|
challengeType: "auto",
|
||||||
},
|
},
|
||||||
strategy: {
|
strategy: {
|
||||||
runStrategy: 0, // 正常执行
|
runStrategy: 0, // 正常执行
|
||||||
|
|||||||
Reference in New Issue
Block a user