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

This commit is contained in:
xiaojunnuo
2025-07-15 10:45:34 +08:00
126 changed files with 2024 additions and 562 deletions
+13
View File
@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.6](https://github.com/certd/certd/compare/v1.36.5...v1.36.6) (2025-07-14)
### Bug Fixes
* 修复某些页面翻译不全显示错误的bug ([0b3158f](https://github.com/certd/certd/commit/0b3158fdd5fe5bb0a98c4e65715dbc3de2c38047))
* 修复运行流水线后会闪烁一下的bug ([dfc9362](https://github.com/certd/certd/commit/dfc9362084082ee535b898f23b2609c1d946a6fd))
### Performance Improvements
* 通知和定时器的删除按钮显示为红色更显眼 ([61ba83c](https://github.com/certd/certd/commit/61ba83c77546c3d505d081e19a3d68c127662bf1))
* 优化流水线列表页面、详情页面性能,精简返回数据 ([609ac9c](https://github.com/certd/certd/commit/609ac9c9a2dde605eb09834ae59693c1cb238765))
* OpenAPI支持autoApply参数 ([42f4d14](https://github.com/certd/certd/commit/42f4d1477dc791520a874aed56035abcbc8c433b))
## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11)
**Note:** Version bump only for package @certd/ui-client
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-client",
"version": "1.36.5",
"version": "1.36.6",
"private": true,
"scripts": {
"dev": "vite --open",
@@ -103,8 +103,8 @@
"zod-defaults": "^0.1.3"
},
"devDependencies": {
"@certd/lib-iframe": "^1.36.5",
"@certd/pipeline": "^1.36.5",
"@certd/lib-iframe": "^1.36.6",
"@certd/pipeline": "^1.36.6",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@types/chai": "^4.3.12",
@@ -33,6 +33,7 @@
import { Ref, ref, watch, nextTick } from "vue";
import { HttpRecord } from "/@/components/plugins/cert/domains-verify-plan-editor/type";
import { dict } from "@fast-crud/fast-crud";
import { Dicts } from "/@/components/plugins/lib/dicts";
defineOptions({
name: "HttpVerifyPlan",
@@ -68,17 +69,7 @@ async function onRecordChange() {
emit("change", records.value);
}
const uploaderTypeDict = dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true },
],
});
const uploaderTypeDict = Dicts.uploaderTypeDict;
</script>
<style lang="less">
@@ -0,0 +1,31 @@
import { dict } from "@fast-crud/fast-crud";
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
challengeTypeDict: dict({
data: [
{ value: "dns", label: "DNS校验", color: "green" },
{ value: "cname", label: "CNAME代理校验", color: "blue" },
{ value: "http", label: "HTTP校验", color: "yellow" },
],
}),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
uploaderTypeDict: dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
{ label: "七牛OSS", value: "qiniuoss" },
{ label: "S3/Minio", value: "s3" },
{ label: "SSH(已废弃,请选择SFTP方式)", value: "ssh", disabled: true },
],
}),
};
@@ -471,7 +471,7 @@ export default {
statusError: "Error",
actionImportBatch: "Batch Import",
actionSyncIp: "Sync IP",
TitleSyncIp: "Sync IP",
modalTitleSyncIp: "Sync IP",
modalContentSyncIp: "Are you sure to sync IP?",
notificationSyncComplete: "Sync Complete",
actionCheckAll: "Check All",
@@ -712,4 +712,18 @@ export default {
close: "Close",
viewCertificateTitle: "View Certificate",
},
domain: {
domainManager: "Domain Manager",
domainDescription: "used to auto apply for certificate", //管理域名的校验方式,用于申请证书时自动选择验证方式
domain: "Domain",
challengeType: "Challenge Type",
dnsProviderType: "DNS Provider Type",
dnsProviderAccess: "DNS Provider Access",
httpUploaderType: "HTTP Uploader Type",
httpUploaderAccess: "HTTP Uploader Access",
httpUploadRootDir: "HTTP Upload Root Dir",
disabled: "Disabled",
challengeSetting: "Challenge Setting",
gotoCnameTip: "Please go to CNAME Record Page",
},
};
@@ -14,6 +14,8 @@ export default {
search: "Search",
enabled: "Enabled",
disabled: "Disabled",
enable: "Enable",
disable: "Disable",
edit: "Edit",
delete: "Delete",
create: "Create",
@@ -715,4 +715,18 @@ export default {
close: "关闭",
viewCertificateTitle: "查看证书",
},
domain: {
domainManager: "域名管理",
domainDescription: "管理域名的校验方式,用于申请证书时自动选择验证方式",
domain: "域名",
challengeType: "校验类型",
dnsProviderType: "DNS提供商类型",
dnsProviderAccess: "DNS提供商授权",
httpUploaderType: "上传方式",
httpUploaderAccess: "上传授权信息",
httpUploadRootDir: "网站根路径",
disabled: "禁用/启用",
challengeSetting: "校验配置",
gotoCnameTip: "CNAME域名配置请前往CNAME记录页面添加",
},
};
@@ -14,6 +14,8 @@ export default {
search: "搜索",
enabled: "已启用",
disabled: "已禁用",
enable: "启用",
disable: "禁用",
edit: "修改",
delete: "删除",
create: "新增",
@@ -116,6 +116,17 @@ export const certdResources = [
keepAlive: true,
},
},
{
title: "certd.domain.domainManager",
name: "DomainManager",
path: "/certd/cert/domain",
component: "/certd/cert/domain/index.vue",
meta: {
icon: "ion:globe-outline",
auth: true,
keepAlive: true,
},
},
{
title: "certd.cnameRecord",
name: "CnameRecord",
@@ -2,7 +2,7 @@
import { ref } from "vue";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -55,6 +55,14 @@ export function createAccessApi(from = "user") {
});
},
async GetDictByIds(ids: number[]) {
return await request({
url: apiPrefix + "/getDictByIds",
method: "post",
data: { ids },
});
},
async GetSecretPlain(id: number, key: string) {
return await request({
url: apiPrefix + "/getSecretPlain",
@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { ref } from "vue";
import { getCommonColumnDefine } from "/@/views/certd/access/common";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
@@ -15,7 +15,7 @@ import { defineComponent, onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { createAccessApi } from "/@/views/certd/access/api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "AccessManager",
@@ -0,0 +1,60 @@
import { request } from "/src/api/service";
const apiPrefix = "/cert/domain";
export async function GetList(query: any) {
return await request({
url: apiPrefix + "/page",
method: "post",
data: query,
});
}
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: any) {
return await request({
url: apiPrefix + "/delete",
method: "post",
params: { id },
});
}
export async function GetObj(id: any) {
return await request({
url: apiPrefix + "/info",
method: "post",
params: { id },
});
}
export async function GetDetail(id: any) {
return await request({
url: apiPrefix + "/detail",
method: "post",
params: { id },
});
}
export async function DeleteBatch(ids: any[]) {
return await request({
url: apiPrefix + "/deleteByIds",
method: "post",
data: { ids },
});
}
@@ -0,0 +1,330 @@
import * as api from "./api";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { Dicts } from "/@/components/plugins/lib/dicts";
import { createAccessApi } from "/@/views/certd/access/api";
import { Modal } from "ant-design-vue";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const router = useRouter();
const { t } = useI18n();
const pageRequest = async (query: UserPageQuery): Promise<UserPageRes> => {
return await api.GetList(query);
};
const editRequest = async ({ form, row }: EditReq) => {
form.id = row.id;
const res = await api.UpdateObj(form);
return res;
};
const delRequest = async ({ row }: DelReq) => {
return await api.DelObj(row.id);
};
const addRequest = async ({ form }: AddReq) => {
const res = await api.AddObj(form);
return res;
};
const userStore = useUserStore();
const settingStore = useSettingStore();
const selectedRowKeys: Ref<any[]> = ref([]);
context.selectedRowKeys = selectedRowKeys;
const accessApi = createAccessApi();
const accessDict = dict({
value: "id",
label: "name",
url: "accessDict",
async getNodesByValues(ids: number[]) {
return await accessApi.GetDictByIds(ids);
},
});
const httpUploaderTypeDict = Dicts.uploaderTypeDict;
const dnsProviderTypeDict = dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
});
return {
crudOptions: {
settings: {
plugins: {
//这里使用行选择插件,生成行选择crudOptions配置,最终会与crudOptions合并
rowSelection: {
enabled: true,
order: -2,
before: true,
// handle: (pluginProps,useCrudProps)=>CrudOptions,
props: {
multiple: true,
crossPage: true,
selectedRowKeys,
},
},
},
},
request: {
pageRequest,
addRequest,
editRequest,
delRequest,
},
tabs: {
name: "challengeType",
show: true,
},
rowHandle: {
minWidth: 200,
fixed: "right",
},
form: {
beforeSubmit({ form }) {
if (form.challengeType === "cname") {
throw new Error("CNAME方式请前往CNAME记录页面进行管理");
}
},
},
columns: {
id: {
title: "ID",
key: "id",
type: "number",
column: {
width: 80,
},
form: {
show: false,
},
},
domain: {
title: t("certd.domain.domain"),
type: "text",
search: {
show: true,
},
form: {
required: true,
},
editForm: {
component: {
disabled: false,
},
},
column: {
sorter: true,
},
},
challengeType: {
title: t("certd.domain.challengeType"),
type: "dict-select",
dict: Dicts.challengeTypeDict,
search: {
show: true,
},
form: {
required: true,
valueChange({ value }) {
if (value === "cname") {
Modal.confirm({
title: t("certd.domain.gotoCnameTip"),
async onOk() {
router.push({
path: "/certd/cname/record",
});
crudExpose.getFormWrapperRef().close();
},
});
}
},
},
column: {
sorter: true,
show: false,
},
},
/**
* challengeType varchar(50),
* dnsProviderType varchar(50),
* dnsProviderAccess bigint,
* httpUploaderType varchar(50),
* httpUploaderAccess bigint,
* httpUploadRootDir varchar(512),
*/
dnsProviderType: {
title: t("certd.domain.dnsProviderType"),
type: "dict-select",
dict: dnsProviderTypeDict,
form: {
component: {
name: "DnsProviderSelector",
},
show: compute(({ form }) => {
return form.challengeType === "dns";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
dnsProviderAccess: {
title: t("certd.domain.dnsProviderAccess"),
type: "dict-select",
dict: accessDict,
form: {
component: {
name: "AccessSelector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.dnsProviderType;
}),
},
show: compute(({ form }) => {
return form.challengeType === "dns";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
httpUploaderType: {
title: t("certd.domain.httpUploaderType"),
type: "dict-select",
dict: Dicts.uploaderTypeDict,
form: {
show: compute(({ form }) => {
return form.challengeType === "http";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
httpUploaderAccess: {
title: t("certd.domain.httpUploaderAccess"),
type: "text",
form: {
component: {
name: "AccessSelector",
vModel: "modelValue",
type: compute(({ form }) => {
return form.httpUploaderType;
}),
},
show: compute(({ form }) => {
return form.challengeType === "http";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
httpUploadRootDir: {
title: t("certd.domain.httpUploadRootDir"),
type: "text",
form: {
show: compute(({ form }) => {
return form.challengeType === "http";
}),
required: true,
},
column: {
show: false,
component: {
color: "auto",
},
},
},
challengeSetting: {
title: t("certd.domain.challengeSetting"),
type: "text",
form: { show: false },
column: {
width: 600,
conditionalRender: false,
cellRender({ row }) {
if (row.challengeType === "dns") {
return (
<div class={"flex"}>
<fs-values-format modelValue={row.challengeType} dict={Dicts.challengeTypeDict} color={"auto"}></fs-values-format>
<fs-values-format modelValue={row.dnsProviderType} dict={dnsProviderTypeDict} color={"auto"}></fs-values-format>
<fs-values-format class={"ml-5"} modelValue={row.dnsProviderAccess} dict={accessDict} color={"auto"}></fs-values-format>
</div>
);
} else if (row.challengeType === "http") {
return (
<div class={"flex"}>
<fs-values-format modelValue={row.challengeType} dict={Dicts.challengeTypeDict} color={"auto"}></fs-values-format>
<fs-values-format modelValue={row.httpUploaderType} dict={httpUploaderTypeDict} color={"auto"}></fs-values-format>
<fs-values-format class={"ml-5"} modelValue={row.httpUploaderAccess} dict={accessDict} color={"auto"}></fs-values-format>
<a-tag class={"ml-5 flex items-center"}>{row.httpUploadRootDir}</a-tag>
</div>
);
}
},
},
},
disabled: {
title: t("certd.domain.disabled"),
type: "dict-switch",
search: { show: true },
dict: dict({
data: [
{ label: t("common.enabled"), value: false, color: "green" },
{ label: t("common.disabled"), value: true, color: "red" },
],
}),
form: {
value: false,
required: true,
},
column: {
width: 100,
sorter: true,
},
},
createTime: {
title: t("certd.createTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 160,
align: "center",
},
},
updateTime: {
title: t("certd.updateTime"),
type: "datetime",
form: {
show: false,
},
column: {
show: true,
},
},
},
},
};
}
@@ -0,0 +1,62 @@
<template>
<fs-page class="page-cert">
<template #header>
<div class="title">
{{ t("certd.domain.domainManager") }}
<span class="sub">
{{ t("certd.domain.domainDescription") }}
</span>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding">
<template #pagination-left>
<a-tooltip :title="t('certd.batch_delete')">
<fs-button icon="DeleteOutlined" @click="handleBatchDelete"></fs-button>
</a-tooltip>
</template>
</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";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "DomainManager",
});
const { crudBinding, crudRef, crudExpose, context } = useFs({ createCrudOptions });
const selectedRowKeys = context.selectedRowKeys;
const handleBatchDelete = () => {
if (selectedRowKeys.value?.length > 0) {
Modal.confirm({
title: t("certd.confirm"),
content: t("certd.confirm_delete_count", { count: selectedRowKeys.value.length }),
async onOk() {
await DeleteBatch(selectedRowKeys.value);
message.info(t("certd.delete_successful"));
crudExpose.doRefresh();
selectedRowKeys.value = [];
},
});
} else {
message.error(t("certd.please_select_records"));
}
};
// 页面打开后获取列表数据
onMounted(() => {
crudExpose.doRefresh();
});
onActivated(async () => {
await crudExpose.doRefresh();
});
</script>
<style lang="less"></style>
@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
@@ -26,7 +26,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
@@ -19,7 +19,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -6,7 +6,7 @@
<script setup lang="ts">
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import { CrudOptions, useColumns, useFormWrapper } from "@fast-crud/fast-crud";
@@ -79,7 +79,7 @@ import { UserTwoFactorSetting } from "./api";
import { Modal, notification } from "ant-design-vue";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
const settingsStore = useSettingStore();
@@ -29,7 +29,7 @@
import * as api from "./api";
import { Ref, ref } from "vue";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { useUserProfile } from "./use";
const { t } = useI18n();
@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
//
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { certInfoApi } from "./api";
@@ -14,7 +14,7 @@
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { AddReq, ColumnCompositionProps, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import dayjs from "dayjs";
@@ -46,6 +46,20 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
const { openSiteIpMonitorDialog } = useSiteIpMonitor();
const { openSiteImportDialog } = useSiteImport();
function checkAll() {
Modal.confirm({
title: t("certd.monitor.confirmTitle"), // "确认"
content: t("certd.monitor.confirmContent"), // "确认触发检查全部站点证书吗?"
onOk: async () => {
await siteInfoApi.CheckAll();
notification.success({
message: t("certd.monitor.checkSubmitted"), // "检查任务已提交"
description: t("certd.monitor.pleaseRefresh"), // "请稍后刷新页面查看结果"
});
},
});
}
return {
id: "siteMonitorCrud",
crudOptions: {
@@ -114,6 +128,14 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
});
},
},
checkAll: {
show: true,
text: t("certd.monitor.checkAll"),
type: "primary",
click() {
checkAll();
},
},
},
},
rowHandle: {
@@ -14,9 +14,6 @@
</div>
</div>
</div>
<div class="more">
<a-button type="primary" @click="checkAll">{{ t("certd.monitor.checkAll") }}</a-button>
</div>
</template>
<fs-crud ref="crudRef" v-bind="crudBinding"> </fs-crud>
</fs-page>
@@ -28,26 +25,13 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { siteInfoApi } from "./api";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
name: "SiteCertMonitor",
});
const { crudBinding, crudRef, crudExpose } = useFs({ createCrudOptions, context: {} });
function checkAll() {
Modal.confirm({
title: t("certd.monitor.confirmTitle"), // "确认"
content: t("certd.monitor.confirmContent"), // "确认触发检查全部站点证书吗?"
onOk: async () => {
await siteInfoApi.CheckAll();
notification.success({
message: t("certd.monitor.checkSubmitted"), // "检查任务已提交"
description: t("certd.monitor.pleaseRefresh"), // "请稍后刷新页面查看结果"
});
},
});
}
// 页面打开后获取列表数据
onMounted(() => {
@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { siteIpApi } from "./api";
import dayjs from "dayjs";
@@ -47,7 +47,7 @@ import { merge } from "lodash-es";
import { useSettingStore } from "/src/store/settings";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useUserStore } from "/@/store/user";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -1,6 +1,6 @@
import { useFormWrapper } from "@fast-crud/fast-crud";
import { siteInfoApi } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export function useSiteImport() {
const { t } = useI18n();
@@ -41,7 +41,7 @@ import { dict } from "@fast-crud/fast-crud";
import createCrudOptions from "../crud";
import { notificationProvide } from "/@/views/certd/notification/common";
import { useUserStore } from "/@/store/user";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -1,5 +1,5 @@
// @ts-ignore
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { OPEN_API_DOC, openkeyApi } from "./api";
import { useModal } from "/@/use/use-modal";
@@ -1,14 +0,0 @@
import { dict } from "@fast-crud/fast-crud";
export const Dicts = {
sslProviderDict: dict({
data: [
{ value: "letsencrypt", label: "Lets Encrypt" },
{ value: "zerossl", label: "ZeroSSL" },
],
}),
challengeTypeDict: dict({ data: [{ value: "dns", label: "DNS校验" }] }),
dnsProviderTypeDict: dict({
url: "pi/dnsProvider/dnsProviderTypeDict",
}),
};
@@ -125,6 +125,7 @@ export function useCertPipelineCreator() {
const pluginStore = usePluginStore();
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
const groupDictRef = dict({
url: "/pi/pipeline/group/all",
@@ -193,7 +194,7 @@ export function useCertPipelineCreator() {
title: t("certd.pipelineForm.triggerCronTitle"),
type: "text",
form: {
value: `0 ${randomMin} ${randomHour} * * *`,
value: randomCron,
component: {
name: "cron-editor",
vModel: "modelValue",
@@ -5,7 +5,7 @@
<script setup lang="ts">
import * as api from "../api";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -335,7 +335,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
}
},
},
_triggerCount: {
triggerCount: {
title: t("certd.fields.scheduledTaskCount"),
type: "number",
column: {
@@ -346,7 +346,7 @@ export default function ({ crudExpose, context: { groupDictRef, selectedRowKeys
show: false,
},
},
_stepCount: {
stepCount: {
title: t("certd.fields.deployTaskCount"),
type: "number",
form: { show: false },
@@ -31,7 +31,7 @@ import ChangeGroup from "./components/change-group.vue";
import ChangeTrigger from "./components/change-trigger.vue";
import { Modal, notification } from "ant-design-vue";
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import ChangeNotification from "/@/views/certd/pipeline/components/change-notification.vue";
@@ -4,7 +4,7 @@
<fs-icon v-bind="status" :color="status.iconColor || status.color" />
</template>
<p>
<fs-date-format :model-value="runnable.status?.startTime"></fs-date-format>
<fs-date-format :model-value="runnable.createTime"></fs-date-format>
<a-tag class="ml-5" :color="status.color" :closable="status.value === 'start'" @close="cancelTask">
{{ status.label }}
</a-tag>
@@ -46,7 +46,7 @@ export default defineComponent({
emits: ["view", "cancel"],
setup(props: any, ctx: any) {
const status = computed(() => {
return statusUtil.get(props.runnable?.status?.result);
return statusUtil.get(props.runnable?.status);
});
function view() {
@@ -3,7 +3,7 @@
<template #title>
<div>
{{ t("certd.edit_notification") }}
<a-button v-if="mode === 'edit'" @click="notificationDelete()">
<a-button v-if="mode === 'edit'" danger @click="notificationDelete()">
<template #icon>
<DeleteOutlined />
</template>
@@ -90,7 +90,7 @@ import * as _ from "lodash-es";
import { nanoid } from "nanoid";
import PiNotificationFormEmail from "./pi-notification-form-email.vue";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { cloneDeep } from "lodash-es";
const { t } = useI18n();
@@ -25,7 +25,7 @@
import { Ref, ref, watch } from "vue";
import { useUserStore } from "/@/store/user";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -3,7 +3,7 @@
<template #title>
<div>
编辑任务
<a-button v-if="editMode" @click="taskDelete()">
<a-button v-if="editMode" danger @click="taskDelete()">
<template #icon><DeleteOutlined /></template>
</a-button>
</div>
@@ -3,7 +3,7 @@
<template #title>
<div>
{{ t("certd.editTrigger") }}
<a-button v-if="mode === 'edit'" @click="triggerDelete()">
<a-button v-if="mode === 'edit'" danger @click="triggerDelete()">
<template #icon>
<DeleteOutlined />
</template>
@@ -258,7 +258,7 @@
<a-timeline class="mt-10">
<template v-for="item of histories" :key="item.id">
<pi-history-timeline-item
:runnable="item.pipeline"
:runnable="item"
:history-id="item.id"
:is-current="currentHistory?.id === item.id"
:edit-mode="editMode"
@@ -280,7 +280,7 @@
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, provide, Ref, ref, watch, computed } from "vue";
import { computed, defineComponent, onMounted, onUnmounted, provide, ref, Ref, watch } from "vue";
import { useRouter } from "vue-router";
import PiTaskForm from "./component/task-form/index.vue";
import PiTriggerForm from "./component/trigger-form/index.vue";
@@ -288,7 +288,7 @@ import PiNotificationForm from "./component/notification-form/index.vue";
import PiTaskView from "./component/task-view/index.vue";
import PiStatusShow from "./component/status-show.vue";
import VDraggable from "vuedraggable";
import * as _ from "lodash-es";
import { cloneDeep, merge, remove } from "lodash-es";
import { message, Modal, notification } from "ant-design-vue";
import { nanoid } from "nanoid";
import { PipelineDetail, PipelineOptions, RunHistory } from "./type";
@@ -299,9 +299,10 @@ import { useSettingStore } from "/@/store/settings";
import { useUserStore } from "/@/store/user";
import TaskShortcuts from "./component/shortcut/task-shortcuts.vue";
import { eachSteps, findStep } from "../utils";
import { PluginGroups, usePluginStore } from "/@/store/plugin";
import { usePluginStore } from "/@/store/plugin";
import { getCronNextTimes } from "/@/components/cron-editor/utils";
import { useCertViewer } from "/@/views/certd/pipeline/use";
import { useI18n } from "/@/locales";
export default defineComponent({
name: "PipelineEdit",
@@ -339,6 +340,7 @@ export default defineComponent({
},
emits: ["update:modelValue", "update:editMode"],
setup(props, ctx) {
const { t } = useI18n();
const currentPipeline: Ref<any> = ref({});
const pipeline: Ref<any> = ref({});
@@ -371,18 +373,19 @@ export default defineComponent({
const loadCurrentHistoryDetail = async () => {
const detail: RunHistory = await props.options?.getHistoryDetail({ historyId: currentHistory.value.id });
currentHistory.value.logs = detail.logs;
_.merge(currentHistory.value.pipeline, detail.pipeline);
currentHistory.value.pipeline = detail.pipeline;
};
const changeCurrentHistory = async (history?: RunHistory) => {
if (!history) {
//取消历史记录查看模式
currentHistory.value = null;
pipeline.value = currentPipeline.value;
pipeline.value = cloneDeep(currentPipeline.value);
return;
}
currentHistory.value = history;
pipeline.value = history.pipeline;
await loadCurrentHistoryDetail();
pipeline.value = currentHistory.value.pipeline;
currentPipeline.value = cloneDeep(pipeline.value);
};
async function loadHistoryList(reload = false) {
@@ -413,7 +416,8 @@ export default defineComponent({
return true;
}
}
if (historyList[0].pipeline?.version === pipeline.value.version) {
//@ts-ignore
if (historyList[0]?.version === pipeline.value.version) {
await changeCurrentHistory(historyList[0]);
}
}
@@ -477,7 +481,7 @@ export default defineComponent({
return;
}
const detail: PipelineDetail = await props.options.getPipelineDetail({ pipelineId: value });
currentPipeline.value = _.merge(
currentPipeline.value = merge(
{
title: "新管道流程",
stages: [],
@@ -540,7 +544,7 @@ export default defineComponent({
};
const taskCopy = (stage: any, stageIndex: number, task: any) => {
task = _.cloneDeep(task);
task = cloneDeep(task);
task.id = nanoid();
task.title = task.title + "_copy";
for (const step of task.steps) {
@@ -560,7 +564,7 @@ export default defineComponent({
if (type === "delete") {
stage.tasks.splice(taskIndex, 1);
if (stage.tasks.length === 0) {
_.remove(pipeline.value.stages, (item: Runnable) => {
remove(pipeline.value.stages, (item: Runnable) => {
return item.id === stage.id;
});
}
@@ -666,9 +670,19 @@ export default defineComponent({
notificationFormRef.value.notificationView(notification, (type: string, value: any) => {});
}
};
const notificationDelete = (notification: any, index: any) => {
Modal.confirm({
title: t("certd.confirm"),
content: t("certd.confirm_delete_trigger"),
async onOk() {
pipeline.value.notifications.splice(index, 1);
},
});
};
return {
notificationAdd,
notificationEdit,
notificationDelete,
notificationFormRef,
};
}
@@ -788,7 +802,7 @@ export default defineComponent({
currentPipeline.value = pipeline.value;
//移除空阶段
_.remove(pipeline.value.stages, (item: Stage) => {
remove(pipeline.value.stages, (item: Stage) => {
return item.tasks.length === 0;
});
@@ -802,12 +816,12 @@ export default defineComponent({
}
};
const edit = () => {
pipeline.value = _.cloneDeep(currentPipeline.value);
pipeline.value = cloneDeep(currentPipeline.value);
currentHistory.value = null;
toggleEditMode(true);
};
const cancel = () => {
pipeline.value = currentPipeline.value;
pipeline.value = cloneDeep(currentPipeline.value);
toggleEditMode(false);
};
@@ -1,7 +1,7 @@
import * as api from "./api";
import { Ref, ref } from "vue";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -22,7 +22,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -17,7 +17,7 @@
import { onActivated, onMounted } from "vue";
import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
defineOptions({
name: "PipelineTemplate",
});
@@ -1,33 +1,33 @@
<template>
<a-modal v-model:open="openRef" class="order-modal" :title="$t('order.confirmTitle')" @ok="orderCreate">
<a-modal v-model:open="openRef" class="order-modal" :title="$t('certd.order.confirmTitle')" @ok="orderCreate">
<div v-if="product" class="order-box">
<div class="flex-o mt-5">
<span class="label">{{ $t("order.package") }}</span>{{ product.title }}
<span class="label">{{ $t("certd.order.package") }}</span>{{ product.title }}
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.description") }}</span>{{ product.intro }}
<span class="label">{{ $t("certd.order.description") }}</span>{{ product.intro }}
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.specifications") }}</span>
<span class="label">{{ $t("certd.order.specifications") }}</span>
<span class="flex-o flex-wrap">
<span class="flex-o"> {{ $t("order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" unit="{{$t('order.unit.pieces')}}" /> </span>
<span class="flex-o"> {{ $t("order.domain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" unit="{{$t('order.unit.count')}}" /> </span>
<span class="flex-o"> {{ $t("order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" unit="{{$t('order.unit.times')}}" /> </span>
<span class="flex-o"> {{ $t("certd.order.pipeline") }}<suite-value class="ml-5" :model-value="product.content.maxPipelineCount" :unit="$t('certd.order.unit.pieces')" /> </span>
<span class="flex-o"> {{ $t("certd.order.domain") }}<suite-value class="ml-5" :model-value="product.content.maxDomainCount" :unit="$t('certd.order.unit.count')" /> </span>
<span class="flex-o"> {{ $t("certd.order.deployTimes") }}<suite-value class="ml-5" :model-value="product.content.maxDeployCount" :unit="$t('certd.order.unit.times')" /> </span>
</span>
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.duration") }}</span>
<span class="label">{{ $t("certd.order.duration") }}</span>
<duration-value v-model="formRef.duration"></duration-value>
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.price") }}</span>
<span class="label">{{ $t("certd.order.price") }}</span>
<price-input :edit="false" :model-value="durationSelected.price"></price-input>
</div>
<div class="flex-o mt-5">
<span class="label">{{ $t("order.paymentMethod") }}</span>
<div v-if="durationSelected.price === 0">{{ $t("order.free") }}</div>
<span class="label">{{ $t("certd.order.paymentMethod") }}</span>
<div v-if="durationSelected.price === 0">{{ $t("certd.order.free") }}</div>
<fs-dict-select v-else v-model:value="formRef.payType" :dict="paymentsDictRef" style="width: 200px"> </fs-dict-select>
</div>
</div>
@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
@@ -149,7 +149,7 @@ import { UserInfoRes } from "/@/store/user/api.user";
import { GetStatisticCount } from "/@/views/framework/home/dashboard/api";
import { useRouter } from "vue-router";
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
import { usePluginStore } from "/@/store/plugin";
defineOptions({
@@ -214,6 +214,7 @@ function transformStatusCount() {
{ name: "error", label: "失败" },
{ name: "canceled", label: "已取消" },
{ name: null, label: "未执行" },
{ name: "skip", label: "跳过" },
];
const result = [];
for (const item of sorted) {
@@ -43,7 +43,7 @@ const slots = defineSlots();
.data-item {
display: flex;
flex-direction: column;
height: 180px;
height: 188px;
.header {
display: flex;
justify-content: space-between;
@@ -12,7 +12,7 @@ import { useUserStore } from "/@/store/user";
import ChangePasswordButton from "/@/views/certd/mine/change-password-button.vue";
import { onMounted, ref } from "vue";
import { Modal } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -1,6 +1,6 @@
import * as api from "./api.js";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -28,7 +28,7 @@
import { utils } from "@fast-crud/fast-crud";
import { cloneDeep } from "lodash-es";
import { computed, defineComponent, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "FsPermissionTree",
@@ -19,7 +19,7 @@ import createCrudOptions from "./crud.js";
import FsPermissionTree from "./fs-permission-tree.vue";
import { usePermission } from "/src/plugin/permission";
import { useFs, useUi } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default defineComponent({
name: "AuthorityManager",
@@ -1,6 +1,6 @@
import * as api from "./api";
import { AddReq, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context: { authz } }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -19,7 +19,7 @@ import * as api from "./api";
import { message } from "ant-design-vue";
import FsPermissionTree from "../permission/fs-permission-tree.vue";
import { UseCrudPermissionCompProps, UseCrudPermissionExtraProps } from "/@/plugin/permission";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
function useAuthz() {
const checkedKeys = ref();
@@ -4,7 +4,7 @@ import { useUserStore } from "/@/store/user";
import { Modal, notification } from "ant-design-vue";
import dayjs from "dayjs";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -207,8 +207,8 @@ export default function ({ crudExpose }: CreateCrudOptionsProps): CreateCrudOpti
type: "dict-switch",
dict: dict({
data: [
{ label: t("certd.enabled"), value: 1, color: "green" },
{ label: t("certd.disabled"), value: 0, color: "red" },
{ label: t("common.enabled"), value: 1, color: "green" },
{ label: t("common.disabled"), value: 0, color: "red" },
],
}),
column: {
@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
@@ -27,7 +27,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, useFormWrapper, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
@@ -22,7 +22,7 @@ import { useFs } from "@fast-crud/fast-crud";
import createCrudOptions from "./crud";
import { message, Modal } from "ant-design-vue";
import { DeleteBatch } from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -77,7 +77,7 @@ import * as emailApi from "./api.email";
import { notification } from "ant-design-vue";
import { useSettingStore } from "/src/store/settings";
import * as _ from "lodash-es";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
@@ -1,4 +1,4 @@
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, EditReq, UserPageQuery, UserPageRes } from "@fast-crud/fast-crud";
@@ -62,7 +62,7 @@ import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { util } from "/@/utils";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -73,7 +73,7 @@ import * as api from "/@/views/sys/settings/api";
import { merge } from "lodash-es";
import { useSettingStore } from "/@/store/settings";
import { notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
@@ -56,7 +56,7 @@ import { Modal, notification } from "ant-design-vue";
import { request } from "/@/api/service";
import { util, utils } from "/@/utils";
import { useSettingStore } from "/@/store/settings";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
const { t } = useI18n();
defineOptions({
@@ -4,7 +4,7 @@ import SuiteValue from "./suite-value.vue";
import SuiteValueEdit from "./suite-value-edit.vue";
import PriceEdit from "./price-edit.vue";
import DurationPriceValue from "/@/views/sys/suite/product/duration-price-value.vue";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
export default function ({ crudExpose, context }: CreateCrudOptionsProps): CreateCrudOptionsRet {
const { t } = useI18n();
@@ -55,15 +55,15 @@ export default function ({ crudExpose, context }: CreateCrudOptionsProps): Creat
groups: {
base: {
header: t("certd.basicInfo"),
columns: [t("certd.titlea"), t("certd.type"), t("certd.disabled"), t("certd.ordera"), t("certd.supportBuy"), t("certd.intro")],
columns: ["title", "type", "disabled", "order", "supportBuy", "intro"]
},
content: {
header: t("certd.packageContent"),
columns: [t("certd.maxDomainCount"), t("certd.maxPipelineCount"), t("certd.maxDeployCount"), t("certd.maxMonitorCount")],
columns: ["content.maxDomainCount", "content.maxPipelineCount", "content.maxDeployCount", "content.maxMonitorCount"]
},
price: {
header: t("certd.price"),
columns: [t("certd.durationPrices")],
columns: ["durationPrices"]
},
},
},
@@ -1,5 +1,5 @@
import * as api from "./api";
import { useI18n } from "vue-i18n";
import { useI18n } from "/src/locales";
import { computed, Ref, ref } from "vue";
import { useRouter } from "vue-router";
import { AddReq, compute, CreateCrudOptionsProps, CreateCrudOptionsRet, DelReq, dict, EditReq, UserPageQuery, UserPageRes, utils } from "@fast-crud/fast-crud";
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.36.6](https://github.com/certd/certd/compare/v1.36.5...v1.36.6) (2025-07-14)
### Performance Improvements
* 优化流水线列表页面、详情页面性能,精简返回数据 ([609ac9c](https://github.com/certd/certd/commit/609ac9c9a2dde605eb09834ae59693c1cb238765))
* 支持自动选择校验方式申请证书 ([3f99432](https://github.com/certd/certd/commit/3f9943270cfb12946e38e6272bc5e8d95ad6ab9e))
* OpenAPI支持autoApply参数 ([42f4d14](https://github.com/certd/certd/commit/42f4d1477dc791520a874aed56035abcbc8c433b))
## [1.36.5](https://github.com/certd/certd/compare/v1.36.4...v1.36.5) (2025-07-11)
### Bug Fixes
@@ -0,0 +1,20 @@
DROP TABLE IF EXISTS `cd_domain`;
CREATE TABLE `cd_domain`
(
`id` bigint PRIMARY KEY AUTO_INCREMENT NOT NULL,
`user_id` bigint,
`domain` varchar(512),
challenge_type varchar(50),
dns_provider_type varchar(50),
dns_provider_access bigint,
http_uploader_type varchar(50),
http_uploader_access bigint,
http_upload_root_dir varchar(512),
`disabled` boolean NOT NULL DEFAULT false,
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX `index_domain_user_id` ON `cd_domain` (`user_id`);
CREATE INDEX `index_domain_domain` ON `cd_domain` (`domain`);
@@ -0,0 +1,20 @@
DROP TABLE IF EXISTS "cd_domain";
CREATE TABLE "cd_domain"
(
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY NOT NULL,
"user_id" bigint,
"domain" varchar(512),
challenge_type varchar(50),
dns_provider_type varchar(50),
dns_provider_access bigint,
http_uploader_type varchar(50),
http_uploader_access bigint,
http_upload_root_dir varchar(512),
"disabled" boolean NOT NULL DEFAULT (false),
"create_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP),
"update_time" timestamp NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
CREATE INDEX "index_domain_user_id" ON "cd_domain" ("user_id");
CREATE INDEX "index_domain_domain" ON "cd_domain" ("domain");
@@ -0,0 +1,20 @@
DROP TABLE IF EXISTS "cd_domain";
CREATE TABLE "cd_domain"
(
"id" integer PRIMARY KEY AUTOINCREMENT NOT NULL,
"user_id" integer,
"domain" varchar(512),
challenge_type varchar(50),
dns_provider_type varchar(50),
dns_provider_access bigint,
http_uploader_type varchar(50),
http_uploader_access bigint,
http_upload_root_dir varchar(512),
"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_domain_user_id" ON "cd_domain" ("user_id");
CREATE INDEX "index_domain_domain" ON "cd_domain" ("domain");
+14 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@certd/ui-server",
"version": "1.36.5",
"version": "1.36.6",
"description": "fast-server base midway",
"private": true,
"type": "module",
@@ -42,20 +42,20 @@
"@aws-sdk/client-cloudfront": "^3.699.0",
"@aws-sdk/client-iam": "^3.699.0",
"@aws-sdk/client-s3": "^3.705.0",
"@certd/acme-client": "^1.36.5",
"@certd/basic": "^1.36.5",
"@certd/commercial-core": "^1.36.5",
"@certd/acme-client": "^1.36.6",
"@certd/basic": "^1.36.6",
"@certd/commercial-core": "^1.36.6",
"@certd/cv4pve-api-javascript": "^8.4.1",
"@certd/jdcloud": "^1.36.5",
"@certd/lib-huawei": "^1.36.5",
"@certd/lib-k8s": "^1.36.5",
"@certd/lib-server": "^1.36.5",
"@certd/midway-flyway-js": "^1.36.5",
"@certd/pipeline": "^1.36.5",
"@certd/plugin-cert": "^1.36.5",
"@certd/plugin-lib": "^1.36.5",
"@certd/plugin-plus": "^1.36.5",
"@certd/plus-core": "^1.36.5",
"@certd/jdcloud": "^1.36.6",
"@certd/lib-huawei": "^1.36.6",
"@certd/lib-k8s": "^1.36.6",
"@certd/lib-server": "^1.36.6",
"@certd/midway-flyway-js": "^1.36.6",
"@certd/pipeline": "^1.36.6",
"@certd/plugin-cert": "^1.36.6",
"@certd/plugin-lib": "^1.36.6",
"@certd/plugin-plus": "^1.36.6",
"@certd/plus-core": "^1.36.6",
"@huaweicloud/huaweicloud-sdk-cdn": "^3.1.120",
"@huaweicloud/huaweicloud-sdk-core": "^3.1.120",
"@koa/cors": "^5.0.0",
@@ -1,13 +1,15 @@
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CodeException, Constants, EncryptService } from '@certd/lib-server';
import { CertInfoService } from '../../../modules/monitor/service/cert-info-service.js';
import { CertInfo } from '@certd/plugin-cert';
import { OpenKey } from '../../../modules/open/service/open-key-service.js';
import { BaseOpenController } from '../base-open-controller.js';
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
import { CodeException, Constants, EncryptService } from "@certd/lib-server";
import { CertInfo } from "@certd/plugin-cert";
import { OpenKey } from "../../../modules/open/service/open-key-service.js";
import { BaseOpenController } from "../base-open-controller.js";
import { CertInfoFacade } from "../../../modules/monitor/facade/cert-info-facade.js";
import { merge } from "lodash-es";
export type CertGetReq = {
domains?: string;
certId: number;
autoApply?:boolean;
};
/**
@@ -16,7 +18,7 @@ export type CertGetReq = {
@Controller('/api/v1/cert')
export class OpenCertController extends BaseOpenController {
@Inject()
certInfoService: CertInfoService;
certInfoFacade: CertInfoFacade;
@Inject()
encryptService: EncryptService;
@@ -29,10 +31,13 @@ export class OpenCertController extends BaseOpenController {
throw new CodeException(Constants.res.openKeyError);
}
const res: CertInfo = await this.certInfoService.getCertInfo({
const req = merge({}, bean, query)
const res: CertInfo = await this.certInfoFacade.getCertInfo({
userId,
domains: bean.domains || query.domains,
certId: bean.certId || query.certId,
domains: req.domains,
certId: req.certId,
autoApply: req.autoApply??false,
});
return this.ok(res);
}
@@ -0,0 +1,81 @@
import { ALL, Body, Controller, Inject, Post, Provide, Query } from '@midwayjs/core';
import { Constants, CrudController } from '@certd/lib-server';
import {DomainService} from "../../../modules/cert/service/domain-service.js";
/**
*
*/
@Provide()
@Controller('/api/cert/domain')
export class DomainController extends CrudController<DomainService> {
@Inject()
service: DomainService;
getService(): DomainService {
return this.service;
}
@Post('/page', { summary: Constants.per.authOnly })
async page(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const domain = body.query.domain;
delete body.query.domain;
const bq = qb => {
if (domain) {
qb.andWhere('domain like :domain', { domain: `%${domain}%` });
}
};
const pageRet = await this.getService().page({
query: body.query,
page: body.page,
sort: body.sort,
buildQuery: bq,
});
return this.ok(pageRet);
}
@Post('/list', { summary: Constants.per.authOnly })
async list(@Body(ALL) body: any) {
body.query = body.query ?? {};
body.query.userId = this.getUserId();
const list = await this.getService().list(body);
return this.ok(list);
}
@Post('/add', { summary: Constants.per.authOnly })
async add(@Body(ALL) bean: any) {
bean.userId = this.getUserId();
return super.add(bean);
}
@Post('/update', { summary: Constants.per.authOnly })
async update(@Body(ALL) bean: any) {
await this.service.checkUserId(bean.id, this.getUserId());
delete bean.userId;
return super.update(bean);
}
@Post('/info', { summary: Constants.per.authOnly })
async info(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.info(id);
}
@Post('/delete', { summary: Constants.per.authOnly })
async delete(@Query('id') id: number) {
await this.service.checkUserId(id, this.getUserId());
return super.delete(id);
}
@Post('/deleteByIds', { summary: Constants.per.authOnly })
async deleteByIds(@Body(ALL) body: any) {
await this.service.delete(body.ids, {
userId: this.getUserId(),
});
return this.ok();
}
}
@@ -101,4 +101,10 @@ export class AccessController extends CrudController<AccessService> {
const res = await this.service.getSimpleInfo(id);
return this.ok(res);
}
@Post('/getDictByIds', { summary: Constants.per.authOnly })
async getDictByIds(@Body('ids') ids: number[]) {
const res = await this.service.getSimpleByIds(ids, this.getUserId());
return this.ok(res);
}
}
@@ -14,7 +14,7 @@ import {
import {EmailService} from '../../../modules/basic/service/email-service.js';
import {http, HttpRequestConfig, logger, mergeUtils, utils} from '@certd/basic';
import {NotificationService} from '../../../modules/pipeline/service/notification-service.js';
import {TaskServiceBuilder} from "../../../modules/pipeline/service/task-service-getter.js";
import {TaskServiceBuilder} from "../../../modules/pipeline/service/getter/task-service-getter.js";
@Provide()
@Controller('/api/pi/handle')
@@ -1,16 +1,15 @@
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from '@midwayjs/core';
import { CommonException, Constants, CrudController, PermissionException } from '@certd/lib-server';
import { PipelineEntity } from '../../../modules/pipeline/entity/pipeline.js';
import { HistoryService } from '../../../modules/pipeline/service/history-service.js';
import { HistoryLogService } from '../../../modules/pipeline/service/history-log-service.js';
import { HistoryEntity } from '../../../modules/pipeline/entity/history.js';
import { HistoryLogEntity } from '../../../modules/pipeline/entity/history-log.js';
import { PipelineService } from '../../../modules/pipeline/service/pipeline-service.js';
import * as fs from 'fs';
import { logger } from '@certd/basic';
import { AuthService } from '../../../modules/sys/authority/service/auth-service.js';
import { SysSettingsService } from '@certd/lib-server';
import { In } from 'typeorm';
import { ALL, Body, Controller, Get, Inject, Post, Provide, Query } from "@midwayjs/core";
import { CommonException, Constants, CrudController, PermissionException, SysSettingsService } from "@certd/lib-server";
import { PipelineEntity } from "../../../modules/pipeline/entity/pipeline.js";
import { HistoryService } from "../../../modules/pipeline/service/history-service.js";
import { HistoryLogService } from "../../../modules/pipeline/service/history-log-service.js";
import { HistoryEntity } from "../../../modules/pipeline/entity/history.js";
import { HistoryLogEntity } from "../../../modules/pipeline/entity/history-log.js";
import { PipelineService } from "../../../modules/pipeline/service/pipeline-service.js";
import * as fs from "fs";
import { logger } from "@certd/basic";
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
import { In } from "typeorm";
/**
*
@@ -88,11 +87,30 @@ export class HistoryController extends CrudController<HistoryService> {
const buildQuery = qb => {
qb.limit(20);
};
const withDetail = body.withDetail;
delete body.withDetail;
let select:any = null
if (!withDetail) {
select = {
pipeline: true, // 后面这里改成false
id: true,
userId: true,
pipelineId: true,
status: true,
// startTime: true,
triggerType: true,
endTime: true,
createTime: true,
updateTime: true
};
}
const listRet = await this.getService().list({
query: body,
sort: { prop: 'id', asc: false },
buildQuery,
select
});
for (const item of listRet) {
if (!item.pipeline) {
continue;
@@ -100,7 +118,13 @@ export class HistoryController extends CrudController<HistoryService> {
const json = JSON.parse(item.pipeline);
delete json.stages;
item.pipeline = json;
//@ts-ignore
item.version = json.version;
item.status = json.status.result
delete item.pipeline;
}
return this.ok(listRet);
}
@@ -1,7 +1,8 @@
import {ALL, Body, Controller, Inject, Post, Provide, Query} from '@midwayjs/core';
import {Constants, CrudController} from '@certd/lib-server';
import {SubDomainService, SubDomainsGetter} from "../../../modules/pipeline/service/sub-domain-service.js";
import {SubDomainService} from "../../../modules/pipeline/service/sub-domain-service.js";
import {DomainParser} from '@certd/plugin-cert/dist/dns-provider/domain-parser.js';
import { SubDomainsGetter } from '../../../modules/pipeline/service/getter/sub-domain-getter.js';
/**
*
@@ -0,0 +1,50 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
/**
*
*/
@Entity('cd_domain')
export class DomainEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({ comment: '用户ID', name: 'user_id' })
userId: number;
@Column({ comment: '主域名', length: 100 })
domain: string;
@Column({ comment: '校验类型', name: 'challenge_type', length: 50 })
challengeType : string;
@Column({ comment: 'DNS提供商', name: 'dns_provider_type', length: 50 })
dnsProviderType: string;
@Column({ comment: 'DNS提供商授权', name: 'dns_provider_access' })
dnsProviderAccess: number;
@Column({ comment: '是否禁用', name: 'disabled' })
disabled: boolean;
@Column({ comment: 'http上传类型', name: 'http_uploader_type', length: 50 })
httpUploaderType: string;
@Column({ comment: 'http上传授权', name: 'http_uploader_access' })
httpUploaderAccess: number;
@Column({ comment: 'http上传根目录', name: 'http_upload_root_dir', length: 512 })
httpUploadRootDir: string;
@Column({
comment: '创建时间',
name: 'create_time',
default: () => 'CURRENT_TIMESTAMP',
})
createTime: Date;
@Column({
comment: '修改时间',
name: 'update_time',
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}
@@ -0,0 +1,190 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {In, Not, Repository} from 'typeorm';
import {AccessService, BaseService} from '@certd/lib-server';
import {DomainEntity} from '../entity/domain.js';
import {SubDomainService} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import {DomainVerifiers} from "@certd/plugin-cert";
import { SubDomainsGetter } from '../../pipeline/service/getter/sub-domain-getter.js';
import { CnameRecordService } from '../../cname/service/cname-record-service.js';
import { CnameRecordEntity } from "../../cname/entity/cname-record.js";
/**
*
*/
@Provide()
@Scope(ScopeEnum.Request, {allowDowngrade: true})
export class DomainService extends BaseService<DomainEntity> {
@InjectEntityModel(DomainEntity)
repository: Repository<DomainEntity>;
@Inject()
accessService: AccessService;
@Inject()
subDomainService: SubDomainService;
@Inject()
cnameRecordService: CnameRecordService;
//@ts-ignore
getRepository() {
return this.repository;
}
async add(param) {
if (param.userId == null ){
throw new Error('userId 不能为空');
}
if (!param.domain) {
throw new Error('domain 不能为空');
}
const old = await this.repository.findOne({
where: {
domain: param.domain,
userId: param.userId
}
});
if (old) {
throw new Error(`域名(${param.domain})不能重复`);
}
return await super.add(param);
}
async update(param) {
if (!param.id) {
throw new Error('id 不能为空');
}
const old = await this.info(param.id)
if (!old) {
throw new Error('domain记录不存在');
}
const same = await this.repository.findOne({
where: {
domain: param.domain,
userId: old.userId,
id: Not(param.id)
}
});
if (same) {
throw new Error(`域名(${param.domain})不能重复`);
}
delete param.userId
return await super.update(param);
}
/**
*
* @param userId
* @param domains //去除* 且去重之后的域名列表
*/
async getDomainVerifiers(userId: number, domains: string[]):Promise<DomainVerifiers> {
const mainDomainMap:Record<string, string> = {}
const subDomainGetter = new SubDomainsGetter(userId, this.subDomainService)
const domainParser = new DomainParser(subDomainGetter)
const mainDomains = []
for (const domain of domains) {
const mainDomain = await domainParser.parse(domain);
mainDomainMap[domain] = mainDomain;
mainDomains.push(mainDomain)
}
//匹配DNS记录
let allDomains = [...domains,...mainDomains]
//去重
allDomains = [...new Set(allDomains)]
//从 domain 表中获取配置
const domainRecords = await this.find({
where: {
domain: In(allDomains),
userId,
disabled:false,
}
})
const dnsMap = domainRecords.filter(item=>item.challengeType === 'dns').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
const httpMap = domainRecords.filter(item=>item.challengeType === 'http').reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
//从cname record表中获取配置
const cnameRecords = await this.cnameRecordService.find({
where: {
domain: In(allDomains),
userId,
status: "valid",
}
})
const cnameMap = cnameRecords.reduce((pre, item) => {
pre[item.domain] = item
return pre
}, {})
//构建域名验证计划
const domainVerifiers:DomainVerifiers = {}
for (const domain of domains) {
const mainDomain = mainDomainMap[domain]
const dnsRecord = dnsMap[mainDomain]
if (dnsRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'dns',
dns: {
dnsProviderType: dnsRecord.dnsProviderType,
dnsProviderAccessId: dnsRecord.dnsProviderAccess
}
}
continue
}
const cnameRecord:CnameRecordEntity = cnameMap[domain]
if (cnameRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'cname',
cname: {
domain: cnameRecord.domain,
hostRecord: cnameRecord.hostRecord,
recordValue: cnameRecord.recordValue
}
}
continue
}
const httpRecord = httpMap[domain]
if (httpRecord) {
domainVerifiers[domain] = {
domain,
mainDomain,
type: 'http',
http: {
httpUploaderType: httpRecord.httpUploaderType,
httpUploaderAccess: httpRecord.httpUploaderAccess,
httpUploadRootDir: httpRecord.httpUploadRootDir
}
}
continue
}
domainVerifiers[domain] = null
}
return domainVerifiers;
}
}
@@ -1,18 +1,27 @@
import {Inject, Provide, Scope, ScopeEnum} from '@midwayjs/core';
import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import {AccessService, BaseService, PlusService, ValidateException} from '@certd/lib-server';
import {CnameRecordEntity, CnameRecordStatusType} from '../entity/cname-record.js';
import {createDnsProvider, IDnsProvider} from '@certd/plugin-cert';
import {CnameProvider, CnameRecord} from '@certd/pipeline';
import {cache, http, isDev, logger, utils} from '@certd/basic';
import {getAuthoritativeDnsResolver, walkTxtRecord} from '@certd/acme-client';
import {CnameProviderService} from './cname-provider-service.js';
import {CnameProviderEntity} from '../entity/cname-provider.js';
import {CommonDnsProvider} from './common-provider.js';
import {SubDomainService, SubDomainsGetter} from "../../pipeline/service/sub-domain-service.js";
import {DomainParser} from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import punycode from 'punycode.js'
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import {
AccessService,
BaseService,
PlusService,
SysInstallInfo,
SysSettingsService,
ValidateException
} from "@certd/lib-server";
import { CnameRecordEntity, CnameRecordStatusType } from "../entity/cname-record.js";
import { createDnsProvider, IDnsProvider } from "@certd/plugin-cert";
import { CnameProvider, CnameRecord } from "@certd/pipeline";
import { cache, http, isDev, logger, utils } from "@certd/basic";
import { getAuthoritativeDnsResolver, walkTxtRecord } from "@certd/acme-client";
import { CnameProviderService } from "./cname-provider-service.js";
import { CnameProviderEntity } from "../entity/cname-provider.js";
import { CommonDnsProvider } from "./common-provider.js";
import { DomainParser } from "@certd/plugin-cert/dist/dns-provider/domain-parser.js";
import punycode from "punycode.js";
import { SubDomainService } from "../../pipeline/service/sub-domain-service.js";
import { SubDomainsGetter } from "../../pipeline/service/getter/sub-domain-getter.js";
type CnameCheckCacheValue = {
validating: boolean;
pass: boolean;
@@ -34,6 +43,8 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
@Inject()
cnameProviderService: CnameProviderService;
@Inject()
sysSettingsService: SysSettingsService;
@Inject()
accessService: AccessService;
@@ -100,7 +111,17 @@ export class CnameRecordService extends BaseService<CnameRecordEntity> {
}
param.hostRecord = hostRecord;
const cnameKey = utils.id.simpleNanoId().toLowerCase();
const randomKey = utils.id.simpleNanoId(6).toLowerCase();
let userIdHash = ""
if(param.cnameProviderId < 0){
//公共cname服务
userIdHash = utils.hash.md5(`userId${userId}_${randomKey}`).substring(0, 10)
}else{
const installInfo = await this.sysSettingsService.getSetting<SysInstallInfo>(SysInstallInfo)
userIdHash = utils.hash.md5(`${installInfo.siteId}_${randomKey}`).substring(0, 10)
}
const cnameKey = `${userIdHash}-${randomKey}`;
const safeDomain = param.domain.replaceAll('.', '-');
param.recordValue = `${safeDomain}.${cnameKey}.${cnameProvider.domain}`;
}
@@ -0,0 +1,121 @@
import { Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { CodeException, Constants } from "@certd/lib-server";
import { CertInfoEntity } from "../entity/cert-info.js";
import { utils } from "@certd/basic";
import { PipelineService } from "../../pipeline/service/pipeline-service.js";
import { UserSettingsService } from "../../mine/service/user-settings-service.js";
import { UserEmailSetting } from "../../mine/service/models.js";
import { PipelineEntity } from "../../pipeline/entity/pipeline.js";
import { CertInfoService } from "../service/cert-info-service.js";
import { DomainService } from "../../cert/service/domain-service.js";
import { DomainVerifierGetter } from "../../pipeline/service/getter/domain-verifier-getter.js";
@Provide("CertInfoFacade")
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class CertInfoFacade {
@Inject()
pipelineService: PipelineService;
@Inject()
certInfoService: CertInfoService ;
@Inject()
domainService: DomainService
@Inject()
userSettingsService : UserSettingsService
async getCertInfo(req: { domains?: string; certId?: number; userId: number,autoApply?:boolean }) {
const { domains, certId, userId } = req;
if (certId) {
return await this.certInfoService.getCertInfoById({ id: certId, userId });
}
const domainArr = domains.split(',');
const matchedList = await this.certInfoService.getMatchCertList({domains:domainArr,userId})
let matched: CertInfoEntity = null
if (matchedList.length === 0 ) {
if(req.autoApply === true){
//自动申请,先创建自动申请流水线
const pipeline:PipelineEntity = await this.createAutoPipeline({domains:domainArr,userId})
await this.triggerApplyPipeline({pipelineId:pipeline.id})
}else{
throw new CodeException(Constants.res.openCertNotFound);
}
}
matched = null;
for (const item of matchedList) {
if (item.expiresTime>0 && item.expiresTime < new Date().getTime()) {
matched = item;
break
}
}
if (!matched) {
if(req.autoApply === true){
//如果没有找到有效期内的证书,则自动触发一次申请
const first = matchedList[0]
await this.triggerApplyPipeline({pipelineId:first.pipelineId})
return
}else{
throw new CodeException(Constants.res.openCertNotFound);
}
}
return await this.certInfoService.getCertInfoById({ id: matched.id, userId: userId });
}
async createAutoPipeline(req:{domains:string[],userId:number}){
const verifierGetter = new DomainVerifierGetter(req.userId, this.domainService)
const allDomains = []
for (const item of req.domains) {
allDomains.push(item.replaceAll("*.",""))
}
const verifiers = await verifierGetter.getVerifiers(allDomains)
for (const item of allDomains) {
if (!verifiers[item]){
throw new CodeException({
...Constants.res.openDomainNoVerifier,
message:`域名${item}没有配置校验方式,请先在域名管理页面配置`,
data:{
domain:item
}
});
}
}
const userEmailSetting = await this.userSettingsService.getSetting<UserEmailSetting>(req.userId,UserEmailSetting)
if(!userEmailSetting.list){
throw new CodeException(Constants.res.openEmailNotFound)
}
const email = userEmailSetting.list[0]
return await this.pipelineService.createAutoPipeline({
domains: req.domains,
email,
userId: req.userId,
from:"OpenAPI"
})
}
async triggerApplyPipeline(req:{pipelineId:number}){
//查询流水线状态
const status = await this.pipelineService.getStatus(req.pipelineId)
if (status != 'running') {
await this.pipelineService.trigger(req.pipelineId)
await utils.sleep(1000)
}
throw new CodeException({
...Constants.res.openCertApplying,
data:{
pipelineId:req.pipelineId
}
});
}
}
@@ -1,10 +1,10 @@
import {Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {BaseService, CodeException, Constants, PageReq} from "@certd/lib-server";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {MoreThan, Repository} from "typeorm";
import {CertInfoEntity} from "../entity/cert-info.js";
import {utils} from "@certd/basic";
import {CertInfo, CertReader} from "@certd/plugin-cert";
import { Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { BaseService, CodeException, Constants, PageReq } from "@certd/lib-server";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { Repository } from "typeorm";
import { CertInfoEntity } from "../entity/cert-info.js";
import { utils } from "@certd/basic";
import { CertInfo, CertReader } from "@certd/plugin-cert";
export type UploadCertReq = {
id?: number;
@@ -71,6 +71,7 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
}
await this.addOrUpdate(bean);
return bean.id
}
async deleteByPipelineId(id: number) {
@@ -82,44 +83,28 @@ export class CertInfoService extends BaseService<CertInfoEntity> {
});
}
async getCertInfo(params: { domains?: string; certId?: number; userId: number }) {
const { domains, certId, userId } = params;
if (certId) {
return await this.getCertInfoById({ id: certId, userId });
}
return await this.getCertInfoByDomains({
domains,
userId,
});
}
private async getCertInfoByDomains(params: { domains: string; userId: number }) {
async getMatchCertList(params: { domains: string[]; userId: number }) {
const { domains, userId } = params;
if (!domains) {
throw new CodeException(Constants.res.openCertNotFound);
}
const domainArr = domains.split(',');
const list = await this.find({
select: {
id: true,
domains: true,
expiresTime:true,
pipelineId:true,
},
where: {
userId,
expiresTime: MoreThan(new Date().getTime())
},
});
//遍历查找
const matched = list.find(item => {
return list.filter(item => {
const itemDomains = item.domains.split(',');
return utils.domain.match(domainArr, itemDomains);
return utils.domain.match(domains, itemDomains);
});
if (!matched) {
throw new CodeException(Constants.res.openCertNotFound);
}
return await this.getCertInfoById({ id: matched.id, userId: userId });
}
async getCertInfoById(req: { id: number; userId: number }) {
@@ -0,0 +1,17 @@
import {DomainVerifiers, IDomainVerifierGetter} from "@certd/plugin-cert";
import {DomainService} from "../../../cert/service/domain-service.js";
export class DomainVerifierGetter implements IDomainVerifierGetter {
private userId: number;
private domainService: DomainService;
constructor(userId: number, domainService: DomainService) {
this.userId = userId;
this.domainService = domainService;
}
async getVerifiers(domains: string[]): Promise<DomainVerifiers>{
return await this.domainService.getDomainVerifiers(this.userId,domains);
}
}
@@ -1,5 +1,5 @@
import { INotificationService, NotificationSendReq } from '@certd/pipeline';
import { NotificationService } from './notification-service.js';
import {NotificationService} from "../notification-service.js";
export class NotificationGetter implements INotificationService {
userId: number;
@@ -0,0 +1,17 @@
import {ISubDomainsGetter} from "@certd/plugin-cert";
import {SubDomainService} from "../sub-domain-service.js";
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
subDomainService: SubDomainService;
constructor(userId: number, subDomainService: SubDomainService) {
this.userId = userId;
this.subDomainService = subDomainService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId)
}
}
@@ -0,0 +1,80 @@
import { IServiceGetter } from "@certd/pipeline";
import { ApplicationContext, IMidwayContainer, Provide, Scope, ScopeEnum } from "@midwayjs/core";
import { AccessGetter, AccessService } from "@certd/lib-server";
import { CnameProxyService } from "./cname-proxy-service.js";
import { NotificationGetter } from "./notification-getter.js";
import { NotificationService } from "../notification-service.js";
import { CnameRecordService } from "../../../cname/service/cname-record-service.js";
import { SubDomainsGetter } from "./sub-domain-getter.js";
import { DomainVerifierGetter } from "./domain-verifier-getter.js";
import { DomainService } from "../../../cert/service/domain-service.js";
import { SubDomainService } from "../sub-domain-service.js";
export class TaskServiceGetter implements IServiceGetter{
private userId: number;
private appCtx : IMidwayContainer;
constructor(userId:number,appCtx:IMidwayContainer) {
this.userId = userId;
this.appCtx = appCtx
}
async get<T>(serviceName: string): Promise<T> {
if(serviceName === 'subDomainsGetter'){
return await this.getSubDomainsGetter() as T
} if (serviceName === 'accessService') {
return await this.getAccessService() as T
} else if (serviceName === 'cnameProxyService') {
return await this.getCnameProxyService() as T
} else if (serviceName === 'notificationService') {
return await this.getNotificationService() as T
} else if (serviceName === 'domainVerifierGetter') {
return await this.getDomainVerifierGetter() as T
}else{
throw new Error(`service ${serviceName} not found`)
}
}
async getSubDomainsGetter(): Promise<SubDomainsGetter> {
const subDomainsService:SubDomainService = await this.appCtx.getAsync("subDomainService")
return new SubDomainsGetter(this.userId, subDomainsService)
}
async getAccessService(): Promise<AccessGetter> {
const accessService:AccessService = await this.appCtx.getAsync("accessService")
return new AccessGetter(this.userId, accessService.getById.bind(accessService));
}
async getCnameProxyService(): Promise<CnameProxyService> {
const cnameRecordService:CnameRecordService = await this.appCtx.getAsync("cnameRecordService")
return new CnameProxyService(this.userId, cnameRecordService.getWithAccessByDomain.bind(cnameRecordService));
}
async getNotificationService(): Promise<NotificationGetter> {
const notificationService:NotificationService = await this.appCtx.getAsync("notificationService")
return new NotificationGetter(this.userId, notificationService);
}
async getDomainVerifierGetter(): Promise<DomainVerifierGetter> {
const domainService:DomainService = await this.appCtx.getAsync("domainService")
return new DomainVerifierGetter(this.userId, domainService);
}
}
export type TaskServiceCreateReq = {
userId: number;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
@ApplicationContext()
appCtx: IMidwayContainer;
create(req:TaskServiceCreateReq){
const userId = req.userId;
return new TaskServiceGetter(userId,this.appCtx)
}
}
@@ -1,6 +1,6 @@
import {Config, Inject, Provide, Scope, ScopeEnum, sleep} from "@midwayjs/core";
import {InjectEntityModel} from "@midwayjs/typeorm";
import {In, MoreThan, Repository} from "typeorm";
import { Config, Inject, Provide, Scope, ScopeEnum, sleep } from "@midwayjs/core";
import { InjectEntityModel } from "@midwayjs/typeorm";
import { In, MoreThan, Repository } from "typeorm";
import {
AccessService,
BaseService,
@@ -11,8 +11,8 @@ import {
SysSettingsService,
SysSiteInfo
} from "@certd/lib-server";
import {PipelineEntity} from "../entity/pipeline.js";
import {PipelineDetail} from "../entity/vo/pipeline-detail.js";
import { PipelineEntity } from "../entity/pipeline.js";
import { PipelineDetail } from "../entity/vo/pipeline-detail.js";
import {
Executor,
IAccessService,
@@ -25,33 +25,32 @@ import {
SysInfo,
UserInfo
} from "@certd/pipeline";
import {DbStorage} from "./db-storage.js";
import {StorageService} from "./storage-service.js";
import {Cron} from "../../cron/cron.js";
import {HistoryService} from "./history-service.js";
import {HistoryEntity} from "../entity/history.js";
import {HistoryLogEntity} from "../entity/history-log.js";
import {HistoryLogService} from "./history-log-service.js";
import {EmailService} from "../../basic/service/email-service.js";
import {UserService} from "../../sys/authority/service/user-service.js";
import {CnameRecordService} from "../../cname/service/cname-record-service.js";
import {PluginConfigGetter} from "../../plugin/service/plugin-config-getter.js";
import { DbStorage } from "./db-storage.js";
import { StorageService } from "./storage-service.js";
import { Cron } from "../../cron/cron.js";
import { HistoryService } from "./history-service.js";
import { HistoryEntity } from "../entity/history.js";
import { HistoryLogEntity } from "../entity/history-log.js";
import { HistoryLogService } from "./history-log-service.js";
import { EmailService } from "../../basic/service/email-service.js";
import { UserService } from "../../sys/authority/service/user-service.js";
import { CnameRecordService } from "../../cname/service/cname-record-service.js";
import { PluginConfigGetter } from "../../plugin/service/plugin-config-getter.js";
import dayjs from "dayjs";
import {DbAdapter} from "../../db/index.js";
import {isComm, isPlus} from "@certd/plus-core";
import {logger} from "@certd/basic";
import {UrlService} from "./url-service.js";
import {NotificationService} from "./notification-service.js";
import {UserSuiteEntity, UserSuiteService} from "@certd/commercial-core";
import {CertInfoService} from "../../monitor/service/cert-info-service.js";
import {TaskServiceBuilder} from "./task-service-getter.js";
import {nanoid} from "nanoid";
import {set} from "lodash-es";
import { DbAdapter } from "../../db/index.js";
import { isComm, isPlus } from "@certd/plus-core";
import { logger } from "@certd/basic";
import { UrlService } from "./url-service.js";
import { NotificationService } from "./notification-service.js";
import { UserSuiteEntity, UserSuiteService } from "@certd/commercial-core";
import { CertInfoService } from "../../monitor/service/cert-info-service.js";
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
import { nanoid } from "nanoid";
import { set } from "lodash-es";
const runningTasks: Map<string | number, Executor> = new Map();
/**
*
*/
@@ -91,7 +90,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
@Inject()
cron: Cron;
@Config('certd')
@Config("certd")
private certdConfig: any;
@Inject()
@@ -112,17 +111,33 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async add(bean: PipelineEntity) {
bean.status = ResultType.none
bean.status = ResultType.none;
await this.save(bean);
return bean;
}
async page(pageReq: PageReq<PipelineEntity>) {
//模版流水线不要被查询出来
set(pageReq,"query.isTemplate",false)
set(pageReq, "query.isTemplate", false);
const result = await super.page(pageReq);
await this.fillLastVars(result.records);
for (const item of result.records) {
if (!item.content) {
continue;
}
const pipeline = JSON.parse(item.content);
let stepCount = 0;
RunnableCollection.each(pipeline.stages, (runnable: any) => {
stepCount++;
});
// @ts-ignore
item.stepCount = stepCount;
// @ts-ignore
item.triggerCount = pipeline.triggers.length;
delete item.content;
}
return result;
}
@@ -132,7 +147,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
for (const record of records) {
pipelineIds.push(record.id);
recordMap[record.id] = record;
record.title = record.title + '';
record.title = record.title + "";
}
if (pipelineIds?.length > 0) {
const vars = await this.storageService.findPipelineVars(pipelineIds);
@@ -153,17 +168,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
const info = await this.info(pipelineId);
if (info && !info.disabled) {
const pipeline = JSON.parse(info.content);
this.registerTriggers(pipeline,false);
this.registerTriggers(pipeline, false);
}
}
public async registerTrigger(info:PipelineEntity) {
public async registerTrigger(info: PipelineEntity) {
if (info == null) {
return;
}
if (info && !info.disabled) {
const pipeline = JSON.parse(info.content);
this.registerTriggers(pipeline,false);
this.registerTriggers(pipeline, false);
}
}
@@ -190,12 +205,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
const isUpdate = bean.id > 0 && old != null;
const pipeline = JSON.parse(bean.content || '{}');
const pipeline = JSON.parse(bean.content || "{}");
RunnableCollection.initPipelineRunnableType(pipeline);
let domains = [];
if (pipeline.stages) {
RunnableCollection.each(pipeline.stages, (runnable: any) => {
if (runnable.runnableType === 'step' && runnable.type.indexOf('CertApply')>=0) {
if (runnable.runnableType === "step" && runnable.type.indexOf("CertApply") >= 0) {
domains = runnable.input.domains || [];
}
});
@@ -206,7 +221,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.checkMaxPipelineCount(bean, pipeline, domains);
}
if (!bean.status ){
if (!bean.status) {
bean.status = ResultType.none;
}
if (!isUpdate) {
@@ -217,9 +232,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.doUpdatePipelineJson(bean, pipeline);
//保存域名信息到certInfo表
let fromType = 'pipeline';
if (bean.type === 'cert_upload') {
fromType = 'upload';
let fromType = "pipeline";
if (bean.type === "cert_upload") {
fromType = "upload";
}else if (bean.type === "cert_auto") {
fromType = "auto";
}
await this.certInfoService.updateDomains(pipeline.id, pipeline.userId || bean.userId, domains, fromType);
return bean;
@@ -230,7 +247,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
* @param bean
* @param pipeline
*/
async doUpdatePipelineJson(bean: PipelineEntity, pipeline:Pipeline) {
async doUpdatePipelineJson(bean: PipelineEntity, pipeline: Pipeline) {
await this.clearTriggers(bean);
if (pipeline.title) {
bean.title = pipeline.title;
@@ -261,7 +278,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
throw new NeedSuiteException(`对不起,您最多只能添加${userSuite.domainCount.max}个域名,请购买或升级套餐`);
}
}
}else{
} else {
//非商业版校验用户最大流水线数量
const userId = bean.userId;
const userIsAdmin = await this.userService.isAdmin(userId);
@@ -280,12 +297,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
async foreachPipeline(callback: (pipeline: PipelineEntity) => void) {
const idEntityList = await this.repository.find({
select: {
id: true,
id: true
},
where: {
disabled: false,
templateId: 0,
},
templateId: 0
}
});
const ids = idEntityList.map(item => {
return item.id;
@@ -305,7 +322,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
//分段加载记录
for (const idArr of idsSpan) {
const list = await this.repository.findBy({
id: In(idArr),
id: In(idArr)
});
for (const entity of list) {
@@ -330,14 +347,14 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (onlyAdminUser && entity.userId !== 1) {
return;
}
const pipeline = JSON.parse(entity.content ?? '{}');
const pipeline = JSON.parse(entity.content ?? "{}");
try {
await this.registerTriggers(pipeline, immediateTriggerOnce);
} catch (e) {
logger.error('加载定时trigger失败:', e);
logger.error("加载定时trigger失败:", e);
}
});
logger.info('定时器数量:', this.cron.getTaskSize());
logger.info("定时器数量:", this.cron.getTaskSize());
}
async registerTriggers(pipeline?: Pipeline, immediateTriggerOnce = false) {
@@ -359,18 +376,18 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (isComm()) {
await this.checkHasDeployCount(id, entity.userId);
}
await this.checkUserStatus(entity.userId)
await this.checkUserStatus(entity.userId);
this.cron.register({
name: `pipeline.${id}.trigger.once`,
cron: null,
job: async () => {
logger.info('用户手动启动job');
logger.info("用户手动启动job");
try {
await this.doRun(entity, null, stepId);
} catch (e) {
logger.error('手动job执行失败:', e);
logger.error("手动job执行失败:", e);
}
},
}
});
}
@@ -382,7 +399,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
logger.error(e.message);
await this.update({
id: pipelineId,
status: 'no_deploy_count',
status: "no_deploy_count"
});
}
throw e;
@@ -390,7 +407,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
//@ts-ignore
async delete(id:any) {
async delete(id: any) {
await this.clearTriggers(id);
//TODO 删除storage
// const storage = new DbStorage(pipeline.userId, this.storageService);
@@ -405,11 +422,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (id == null) {
return;
}
let pipeline:PipelineEntity = null
if (typeof id === 'number') {
let pipeline: PipelineEntity = null;
if (typeof id === "number") {
pipeline = await this.info(id);
}else{
pipeline = id
} else {
pipeline = id;
}
if (!pipeline) {
return;
@@ -429,7 +446,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
registerCron(pipelineId, trigger) {
if (pipelineId == null) {
logger.warn('pipelineId为空,无法注册定时任务');
logger.warn("pipelineId为空,无法注册定时任务");
return;
}
@@ -438,11 +455,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
return;
}
cron = cron.trim();
if (cron.startsWith('* *')) {
cron = cron.replace('* *', '0 0');
if (cron.startsWith("* *")) {
cron = cron.replace("* *", "0 0");
}
if (cron.startsWith('*')) {
cron = cron.replace('*', '0');
if (cron.startsWith("*")) {
cron = cron.replace("*", "0");
}
const triggerId = trigger.id;
const name = this.buildCronKey(pipelineId, triggerId);
@@ -451,19 +468,19 @@ export class PipelineService extends BaseService<PipelineEntity> {
name,
cron,
job: async () => {
logger.info('定时任务触发:', pipelineId, triggerId);
logger.info("定时任务触发:", pipelineId, triggerId);
if (pipelineId == null) {
logger.warn('pipelineId为空,无法执行');
logger.warn("pipelineId为空,无法执行");
return;
}
try {
await this.run(pipelineId, triggerId);
} catch (e) {
logger.error('定时job执行失败:', e);
logger.error("定时job执行失败:", e);
}
},
}
});
logger.info('当前定时器数量:', this.cron.getTaskSize());
logger.info("当前定时器数量:", this.cron.getTaskSize());
}
/**
@@ -483,11 +500,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
if (isComm()) {
suite = await this.checkHasDeployCount(id, entity.userId);
}
try{
await this.checkUserStatus(entity.userId)
}catch (e) {
logger.info(e.message)
return
try {
await this.checkUserStatus(entity.userId);
} catch (e) {
logger.info(e.message);
return;
}
@@ -505,7 +522,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
return;
}
if (triggerType === 'timer') {
if (triggerType === "timer") {
if (entity.disabled) {
return;
}
@@ -514,25 +531,25 @@ export class PipelineService extends BaseService<PipelineEntity> {
const onChanged = async (history: RunHistory) => {
//保存执行历史
try {
logger.info('保存执行历史:', history.id);
logger.info("保存执行历史:", history.id);
await this.saveHistory(history);
} catch (e) {
const pipelineEntity = new PipelineEntity();
pipelineEntity.id = id;
pipelineEntity.status = 'error';
pipelineEntity.status = "error";
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity);
logger.error('保存执行历史失败:', e);
logger.error("保存执行历史失败:", e);
throw e;
}
};
const userId = entity.userId;
const historyId = await this.historyService.start(entity,triggerType);
const historyId = await this.historyService.start(entity, triggerType);
const userIsAdmin = await this.userService.isAdmin(userId);
const user: UserInfo = {
id: userId,
role: userIsAdmin ? 'admin' : 'user',
role: userIsAdmin ? "admin" : "user"
};
@@ -543,11 +560,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
const taskServiceGetter = this.taskServiceBuilder.create({
userId,
})
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService")
const notificationGetter =await taskServiceGetter.get<INotificationService>("notificationService")
const cnameProxyService =await taskServiceGetter.get<ICnameProxyService>("cnameProxyService")
userId
});
const accessGetter = await taskServiceGetter.get<IAccessService>("accessService");
const notificationGetter = await taskServiceGetter.get<INotificationService>("notificationService");
const cnameProxyService = await taskServiceGetter.get<ICnameProxyService>("cnameProxyService");
const executor = new Executor({
user,
pipeline,
@@ -561,7 +578,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
notificationService: notificationGetter,
fileRootDir: this.certdConfig.fileRootDir,
sysInfo,
serviceGetter:taskServiceGetter
serviceGetter: taskServiceGetter
});
try {
runningTasks.set(historyId, executor);
@@ -579,7 +596,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
}
} catch (e) {
logger.error('执行失败:', e);
logger.error("执行失败:", e);
// throw e;
} finally {
runningTasks.delete(historyId);
@@ -603,7 +620,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
private getTriggerType(triggerId, pipeline) {
let triggerType = 'user';
let triggerType = "user";
if (triggerId != null) {
//如果不是手动触发
//查找trigger
@@ -613,8 +630,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
this.cron.remove(this.buildCronKey(pipeline.id, triggerId));
triggerType = null;
} else {
logger.info('timer trigger:' + found.id, found.title, found.cron);
triggerType = 'timer';
logger.info("timer trigger:" + found.id, found.title, found.cron);
triggerType = "timer";
}
}
return triggerType;
@@ -637,7 +654,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
//修改pipeline状态
const pipelineEntity = new PipelineEntity();
pipelineEntity.id = parseInt(history.pipeline.id);
pipelineEntity.status = history.pipeline.status.status + '';
pipelineEntity.status = history.pipeline.status.result + "";
pipelineEntity.lastHistoryTime = history.pipeline.status.startTime;
await this.update(pipelineEntity);
@@ -661,8 +678,8 @@ export class PipelineService extends BaseService<PipelineEntity> {
async count(param: { userId?: any }) {
const count = await this.repository.count({
where: {
userId: param.userId,
},
userId: param.userId
}
});
return count;
}
@@ -670,12 +687,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
async statusCount(param: { userId?: any } = {}) {
const statusCount = await this.repository
.createQueryBuilder()
.select('status')
.addSelect('count(1)', 'count')
.select("status")
.addSelect("count(1)", "count")
.where({
userId: param.userId,
userId: param.userId
})
.groupBy('status')
.groupBy("status")
.getRawMany();
return statusCount;
}
@@ -685,11 +702,11 @@ export class PipelineService extends BaseService<PipelineEntity> {
select: {
id: true,
title: true,
status: true,
status: true
},
where: {
userId,
},
userId
}
});
await this.fillLastVars(list);
list = list.filter(item => {
@@ -703,16 +720,16 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async createCountPerDay(param: { days: number } = { days: 7 }) {
const todayEnd = dayjs().endOf('day');
const todayEnd = dayjs().endOf("day");
const result = await this.getRepository()
.createQueryBuilder('main')
.select(`${this.dbAdapter.date('main.createTime')} AS date`) // 将UNIX时间戳转换为日期
.addSelect('COUNT(1) AS count')
.createQueryBuilder("main")
.select(`${this.dbAdapter.date("main.createTime")} AS date`) // 将UNIX时间戳转换为日期
.addSelect("COUNT(1) AS count")
.where({
// 0点
createTime: MoreThan(todayEnd.add(-param.days, 'day').toDate()),
createTime: MoreThan(todayEnd.add(-param.days, "day").toDate())
})
.groupBy('date')
.groupBy("date")
.getRawMany();
return result;
@@ -729,48 +746,48 @@ export class PipelineService extends BaseService<PipelineEntity> {
await this.repository.update(
{
id: In(ids),
userId,
userId
},
{ groupId }
);
}
async batchUpdateTrigger(ids: number[], trigger: any, userId: any){
async batchUpdateTrigger(ids: number[], trigger: any, userId: any) {
const list = await this.find({
where:{
where: {
id: In(ids),
userId
}
})
});
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.triggers = [{
id: nanoid(),
title: '定时触发',
title: "定时触发",
...trigger
}]
await this.doUpdatePipelineJson(item,pipeline)
}];
await this.doUpdatePipelineJson(item, pipeline);
}
}
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any){
async batchUpdateNotifications(ids: number[], notification: Notification, userId: any) {
const list = await this.find({
where:{
where: {
id: In(ids),
userId
}
})
});
for (const item of list) {
const pipeline = JSON.parse(item.content);
pipeline.notifications = [{
id: nanoid(),
title: '通知',
title: "通知",
/**
* type: NotificationType;
* when: NotificationWhen[];
@@ -781,44 +798,44 @@ export class PipelineService extends BaseService<PipelineEntity> {
*/
type: "other",
...notification
}]
await this.doUpdatePipelineJson(item,pipeline)
}];
await this.doUpdatePipelineJson(item, pipeline);
}
}
async batchRerun(ids: number[], userId: any) {
if (!isPlus()){
throw new NeedVIPException("此功能需要升级专业版")
if (!isPlus()) {
throw new NeedVIPException("此功能需要升级专业版");
}
if (!userId || ids.length === 0) {
return;
}
const list = await this.repository.find({
select:{
id:true
select: {
id: true
},
where:{
where: {
id: In(ids),
userId
}
})
});
ids = list.map(item=>item.id)
ids = list.map(item => item.id);
//异步执行
this.startBatchRerun(ids)
this.startBatchRerun(ids);
}
async startBatchRerun(ids: number[]){
async startBatchRerun(ids: number[]) {
//20条一批
const batchSize = 20;
for (let i = 0; i < ids.length; i += batchSize) {
const batchIds = ids.slice(i, i + batchSize);
const batchPromises = batchIds.map(async (id)=>{
await this.run(id,null,"ALL")
const batchPromises = batchIds.map(async (id) => {
await this.run(id, null, "ALL");
});
await Promise.all(batchPromises)
await Promise.all(batchPromises);
}
}
@@ -831,35 +848,132 @@ export class PipelineService extends BaseService<PipelineEntity> {
return await this.repository.find({
select: {
id: true,
title: true,
title: true
},
where: {
id: In(pipelineIds),
userId,
},
userId
}
});
}
private async checkUserStatus(userId: number) {
const userEntity = await this.userService.info(userId);
if(userEntity == null){
throw new Error('用户不存在');
if (userEntity == null) {
throw new Error("用户不存在");
}
if(userEntity.status === 0){
const message = `账户${userId}已被禁用,禁止运行流水线`
throw new Error(message)
if (userEntity.status === 0) {
const message = `账户${userId}已被禁用,禁止运行流水线`;
throw new Error(message);
}
const sysPublic = await this.sysSettingsService.getPublicSettings()
if(isPlus() && sysPublic.userValidTimeEnabled === true){
const sysPublic = await this.sysSettingsService.getPublicSettings();
if (isPlus() && sysPublic.userValidTimeEnabled === true) {
//校验用户有效期是否设置
if(userEntity.validTime!= null && userEntity.validTime > 0){
if(userEntity.validTime < new Date().getTime()){
if (userEntity.validTime != null && userEntity.validTime > 0) {
if (userEntity.validTime < new Date().getTime()) {
//用户已过期
const message = `账户${userId}已过有效期,禁止运行流水线`
throw new Error(message)
const message = `账户${userId}已过有效期,禁止运行流水线`;
throw new Error(message);
}
}
}
}
async createAutoPipeline(req: { domains: string[]; email: string; userId: number ,from:string}) {
const randomHour = Math.floor(Math.random() * 6);
const randomMin = Math.floor(Math.random() * 60);
const randomCron = `0 ${randomMin} ${randomHour} * * *`;
let pipeline: any = {
title: req.domains[0] + `证书自动申请【${req.from??"OpenAPI"}`,
runnableType: "pipeline",
triggers: [
{
id: nanoid(),
title: "定时触发",
props:{
cron: randomCron,
},
type: "timer"
}
],
notifications: [
{
id: nanoid(),
type: "custom",
when: ["error", "turnToSuccess", "success"],
notificationId: 0,
title: "默认通知",
}
],
stages: [
{
id: nanoid(),
title: "证书申请阶段",
maxTaskCount: 1,
runnableType: "stage",
tasks: [
{
id: nanoid(),
title: "证书申请任务",
runnableType: "task",
steps: [
{
id: nanoid(),
title: "申请证书",
runnableType: "step",
input: {
renewDays: 35,
domains: req.domains,
email: req.email,
"challengeType": "auto",
"sslProvider": "letsencrypt",
"privateKeyType": "rsa_2048",
"certProfile": "classic",
"useProxy": false,
"skipLocalVerify": false,
"maxCheckRetryCount": 20,
"waitDnsDiffuseTime": 30,
"pfxArgs": "-macalg SHA1 -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES",
"successNotify": true
},
strategy: {
runStrategy: 0 // 正常执行
},
type: "CertApply"
}
]
}
]
}
]
};
const bean = new PipelineEntity();
bean.title = pipeline.title;
bean.content = JSON.stringify(pipeline);
bean.userId = req.userId;
bean.status = "none";
bean.type = "cert_auto";
bean.disabled = false
bean.keepHistoryCount = 30
await this.save(bean)
return bean;
}
async getStatus(pipelineId: number) {
const res = await this.repository.findOne({
select: {
status: true
},
where: {
id: pipelineId
}
});
return res?.status;
}
}
@@ -4,7 +4,6 @@ import {InjectEntityModel} from '@midwayjs/typeorm';
import {Repository} from 'typeorm';
import {SubDomainEntity} from '../entity/sub-domain.js';
import {EmailService} from '../../basic/service/email-service.js';
import {ISubDomainsGetter} from "@certd/plugin-cert";
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
@@ -38,21 +37,3 @@ export class SubDomainService extends BaseService<SubDomainEntity> {
}
}
export class SubDomainsGetter implements ISubDomainsGetter {
userId: number;
subDomainService: SubDomainService;
constructor(userId: number, subDomainService: SubDomainService) {
this.userId = userId;
this.subDomainService = subDomainService;
}
async getSubDomains() {
return await this.subDomainService.getListByUserId(this.userId)
}
}
@@ -1,63 +0,0 @@
import {IServiceGetter} from "@certd/pipeline";
import {Inject, Provide, Scope, ScopeEnum} from "@midwayjs/core";
import {SubDomainService, SubDomainsGetter} from "./sub-domain-service.js";
import {AccessGetter, AccessService} from "@certd/lib-server";
import {CnameProxyService} from "./cname-proxy-service.js";
import {NotificationGetter} from "./notification-getter.js";
import {NotificationService} from "./notification-service.js";
import {CnameRecordService} from "../../cname/service/cname-record-service.js";
export class TaskServiceGetter implements IServiceGetter{
serviceContainer:Record<string, any>;
constructor(serviceContainer:Record<string, any>) {
this.serviceContainer = serviceContainer;
}
async get<T>(serviceName: string): Promise<T> {
const ret = this.serviceContainer[serviceName] as T;
if(!ret){
throw new Error(`service ${serviceName} not found`)
}
return ret
}
}
export type TaskServiceCreateReq = {
userId: number;
}
export type TaskServiceContainer = {
subDomainsGetter:SubDomainsGetter;
accessService: AccessGetter;
cnameProxyService: CnameProxyService;
notificationService: NotificationGetter;
}
@Provide()
@Scope(ScopeEnum.Request, { allowDowngrade: true })
export class TaskServiceBuilder {
@Inject()
subDomainService: SubDomainService;
@Inject()
accessService: AccessService;
@Inject()
cnameRecordService: CnameRecordService;
@Inject()
notificationService: NotificationService;
create(req:TaskServiceCreateReq){
const userId = req.userId;
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const cnameProxyService = new CnameProxyService(userId, this.cnameRecordService.getWithAccessByDomain.bind(this.cnameRecordService));
const notificationGetter = new NotificationGetter(userId, this.notificationService);
const serviceContainer:TaskServiceContainer = {
subDomainsGetter:new SubDomainsGetter(req.userId, this.subDomainService),
accessService: accessGetter,
cnameProxyService:cnameProxyService,
notificationService:notificationGetter
}
return new TaskServiceGetter(serviceContainer)
}
}