Compare commits

...

20 Commits

Author SHA1 Message Date
xiaojunnuo 32c3ce5c98 perf: 主题默认跟随系统颜色(自动切换深色浅色模式) 2026-02-15 14:57:00 +08:00
xiaojunnuo e55a3a82fc perf: 模版编辑页面,hover反色过亮问题优化 2026-02-15 14:39:11 +08:00
xiaojunnuo 305da86f97 perf: 优化网络测试页面,夜间模式显示效果 2026-02-15 14:23:12 +08:00
xiaojunnuo c23d1d11b5 perf: 监控设置支持逗号分割 2026-02-15 14:20:32 +08:00
xiaojunnuo a3cabd5f36 perf: 列表中支持下次执行时间显示 2026-02-15 14:19:16 +08:00
xiaojunnuo 66ac4716f2 perf: 备份支持scp上传 2026-02-15 14:18:50 +08:00
xiaojunnuo 3cd1aaeb03 perf: 增加部署证书到certd本身插件 2026-02-15 13:24:19 +08:00
xiaojunnuo 4eb940ffe7 perf: http校验方式支持scp上传 2026-02-15 13:16:16 +08:00
xiaojunnuo 61800b23e2 fix: 修复阿里云dcdn使用上传到cas的id引用错误的bug 2026-02-15 13:08:20 +08:00
xiaojunnuo 0283662931 fix: 修复1panel 请求失败的bug 2026-02-15 12:59:08 +08:00
xiaojunnuo 8387fe0d5b fix: 修复保存站点监控dns设置,偶尔无法保存成功的bug 2026-02-13 22:56:59 +08:00
xiaojunnuo b91548eef4 perf: 421 支持3次重试 2026-02-13 19:02:53 +08:00
xiaojunnuo 1195417b97 perf: 优化京东云报错详情显示 2026-02-13 18:16:05 +08:00
xiaojunnuo 8c2dfa9140 chore: 雷池支持上传证书 2026-02-12 21:45:43 +08:00
xiaojunnuo a3fbfe0bff chore: 优化雷池插件的提示说明 2026-02-12 21:26:52 +08:00
xiaojunnuo e7e54bc19e perf: 新网互联支持查询域名列表 2026-02-11 16:27:54 +08:00
xiaojunnuo 9fb980599f fix: 修复任务步骤标题过长导致错位的问题 2026-02-11 15:51:50 +08:00
xiaojunnuo 9642df2d9d build: release 2026-02-10 02:18:23 +08:00
xiaojunnuo 8919a3937a build: publish 2026-02-09 23:12:52 +08:00
xiaojunnuo 5032030f8d build: trigger build image 2026-02-09 23:12:41 +08:00
67 changed files with 412 additions and 121 deletions
+2 -1
View File
@@ -30,4 +30,5 @@ test/**/*.js
/packages/ui/certd-server/data/keys.yaml
/packages/pro/
test.js
.history
.history
/logs
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.38.9](https://github.com/certd/certd/compare/v1.38.8...v1.38.9) (2026-02-09)
### Bug Fixes
* 修复部署到openwrt错误的bug ([9ac33f9](https://github.com/certd/certd/commit/9ac33f9b9ba7727fcbbd320dd866bc048cbb3d72))
* 修复新版本上传到阿里云cas后,其他依赖任务无法部署的bug ([99f5b8e](https://github.com/certd/certd/commit/99f5b8ebc1c64798ceb42042ad71cf71e967beb0))
* esxi部署失败的bug ([6ab1fca](https://github.com/certd/certd/commit/6ab1fcaf894f7ce343af4b5bf4b0d67438df6618))
### Performance Improvements
* 修改sql升级语句,兼容mysql5.7 ([02f89a9](https://github.com/certd/certd/commit/02f89a9c9d77850437285844670aed441e5953c3))
* 已登录状态访问登录页面自动跳转到首页 ([bd8caff](https://github.com/certd/certd/commit/bd8caff0b754cb13530cf0f1644b33e29fde5d01))
* 优化access授权支持remote-auto-complete ([2f40f79](https://github.com/certd/certd/commit/2f40f795ee6131132d3fab2601f92a567bbdc4b7))
* access 插件支持remote-select等配置 ([d286c04](https://github.com/certd/certd/commit/d286c040a5232dcca829945734affead3ee08b3c))
## [1.38.8](https://github.com/certd/certd/compare/v1.38.7...v1.38.8) (2026-02-06)
### Performance Improvements
+1 -1
View File
@@ -70,5 +70,5 @@
"bugs": {
"url": "https://github.com/publishlab/node-acme-client/issues"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -47,5 +47,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+31 -2
View File
@@ -7,7 +7,7 @@ import * as https from "node:https";
import { merge } from "lodash-es";
import { safePromise } from "./util.promise.js";
import fs from "fs";
import sleep from "./util.sleep.js";
const errorMap: Record<string, string> = {
"ssl3_get_record:wrong version number": "http协议错误,服务端要求http协议,请检查是否使用了https请求",
"getaddrinfo EAI_AGAIN": "无法解析域名,请检查网络连接或dns配置,更换docker-compose.yaml中dns配置",
@@ -148,6 +148,16 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
// });
// config.httpsAgent = agent;
config.proxy = false; //必须 否则还会走一层代理,
config.retry = merge(
{
status: [421],
count: 0,
max: 3,
delay: 1000,
},
config.retry
);
return config;
},
(error: Error) => {
@@ -175,7 +185,7 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
}
return response.data;
},
(error: any) => {
async (error: any) => {
const status = error.response?.status;
let message = "";
switch (status) {
@@ -215,6 +225,9 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
case 302:
//重定向
return Promise.resolve(error.response);
case 421:
message = "源站请求超时";
break;
default:
break;
}
@@ -256,6 +269,22 @@ export function createAxiosService({ logger }: { logger: ILogger }) {
if (error instanceof AggregateError) {
logger.error("AggregateError", error);
}
const originalRequest = error.config || {};
logger.info(`config`, originalRequest);
const retry = originalRequest.retry || {};
if (retry.status && retry.status.includes(status)) {
if (retry.max > 0 && retry.count < retry.max) {
// 重试次数增加
retry.count++;
const delay = retry.delay * retry.count;
logger.error(`status=${status},重试次数${retry.count},将在${delay}ms后重试,请求地址:${originalRequest.url}`);
await sleep(delay);
return service.request(originalRequest); // 重试请求
}
logger.error(`重试超过最大次数${retry.max},请求失败:${originalRequest.url}`);
}
const err = new HttpError(error);
if (error.response?.config?.logParams === false) {
delete err.request?.params;
+15 -2
View File
@@ -13,6 +13,19 @@
// await testLocker();
import { domainUtils } from "./dist/utils/util.domain.js";
// import { domainUtils } from "./dist/utils/util.domain.js";
console.log(domainUtils.isIpv6("::0:0:0:FFFF:129.144.52.38"));
// console.log(domainUtils.isIpv6("::0:0:0:FFFF:129.144.52.38"));
// import { http } from "./dist/utils/util.request.js";
// http
// .request({
// url: "https://www.baidu.com/234234/3333",
// retry: {
// status: [404],
// },
// })
// .then(res => {
// console.log(res.data);
// });
+1 -1
View File
@@ -45,5 +45,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+6 -6
View File
@@ -4,13 +4,13 @@ import { HttpClient, ILogger, utils } from "@certd/basic";
import * as _ from "lodash-es";
import { PluginRequestHandleReq } from "../plugin/index.js";
export type AccessRequestHandleReqInput<T = any> = {
id?: number;
title?: string;
access: T;
};
// export type AccessRequestHandleReqInput<T = any> = {
// id?: number;
// title?: string;
// access: T;
// };
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<AccessRequestHandleReqInput<T>>;
export type AccessRequestHandleReq<T = any> = PluginRequestHandleReq<T>;
export type AccessInputDefine = FormItemProps & {
title: string;
+1
View File
@@ -17,6 +17,7 @@ export type PluginRequestHandleReq<T = any> = {
action: string;
input: T;
data: any;
record: { id: number; type: string; title: string };
};
export type UserInfo = {
+1 -1
View File
@@ -24,5 +24,5 @@
"prettier": "^2.8.8",
"tslib": "^2.8.1"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -31,5 +31,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -56,5 +56,5 @@
"fetch"
]
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -32,5 +32,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -64,5 +64,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -46,5 +46,5 @@
"typeorm": "^0.3.11",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -38,5 +38,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
+1 -1
View File
@@ -57,5 +57,5 @@
"tslib": "^2.8.1",
"typescript": "^5.4.2"
},
"gitHead": "4fda6cbcde3d398d7f4dc3ee7e8ea90e691098db"
"gitHead": "b30cb5d7dc8311af4863da7dc8781f7264ba0545"
}
@@ -9,7 +9,7 @@ export function getCronNextTimes(cron: string, count: number = 1) {
const interval = parser.parseExpression(cron);
for (let i = 0; i < count; i++) {
const next = interval.next().getTime();
nextTimes.push(dayjs(next).format("YYYY-MM-DD HH:mm:ss"));
nextTimes.push(dayjs(next).valueOf());
}
return nextTimes;
}
@@ -13,6 +13,7 @@
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { ref, inject } from "vue";
import { Form } from "ant-design-vue";
import { getInputFromForm } from "./utils";
defineOptions({
name: "ApiTest",
@@ -45,13 +46,15 @@ const doTest = async () => {
message.value = "";
hasError.value = false;
loading.value = true;
const { input, record } = getInputFromForm(form, pluginType);
try {
const res = await doRequest(
{
type: pluginType,
typeName: form.type,
action: props.action,
input: pluginType === "plugin" ? form.input : form,
input,
record,
},
{
onError(err: any) {
@@ -16,6 +16,7 @@
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RemoteAutoComplete",
@@ -48,18 +49,6 @@ const message = ref("");
const hasError = ref(false);
const loading = ref(false);
function getInputFromForm(form: any, pluginType: string) {
let input: any = {};
if (pluginType === "plugin") {
input = form?.input || {};
} else if (pluginType === "access") {
input = form?.access || {};
} else {
input = form || {};
}
return input;
}
const getOptions = async () => {
if (loading.value) {
return;
@@ -75,7 +64,7 @@ const getOptions = async () => {
}
const pluginType = getPluginType();
const { form } = getScope();
const input = getInputFromForm(form, pluginType);
const { input, record } = getInputFromForm(form, pluginType);
for (let key in define.input) {
const inWatches = props.watches?.includes(key);
const inputDefine = define.input[key];
@@ -99,6 +88,7 @@ const getOptions = async () => {
action: props.action,
input,
data: {},
record,
},
{
onError(err: any) {
@@ -140,7 +130,7 @@ watch(
() => {
const pluginType = getPluginType();
const { form, key } = getScope();
const input = getInputFromForm(form, pluginType);
const { input, record } = getInputFromForm(form, pluginType);
const watches: any = {};
if (props.watches && props.watches.length > 0) {
for (const key of props.watches) {
@@ -9,6 +9,7 @@ import { doRequest } from "/@/components/plugins/lib";
import { inject, ref, useAttrs } from "vue";
import { useFormWrapper } from "@fast-crud/fast-crud";
import { notification } from "ant-design-vue";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RemoteInput",
@@ -71,15 +72,18 @@ const doPluginFormSubmit = async (data: any) => {
}
loading.value = true;
try {
const pluginType = getPluginType();
const { form } = getScope();
const { input, record } = getInputFromForm(form, pluginType);
const res = await doRequest({
type: pluginType,
typeName: form.type,
action: props.action,
input: pluginType === "plugin" ? form.input : form,
input,
data: data,
record,
});
//获取返回值 填入到input中
emit("update:modelValue", res);
@@ -38,6 +38,7 @@
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RemoteSelect",
@@ -79,17 +80,6 @@ const getPluginType: any = inject("get:plugin:type", () => {
return "plugin";
});
function getInputFromForm(form: any, pluginType: string) {
let input: any = {};
if (pluginType === "plugin") {
input = form?.input || {};
} else if (pluginType === "access") {
input = form?.access || {};
} else {
input = form || {};
}
return input;
}
const searchKeyRef = ref("");
const optionsRef = ref([]);
const message = ref("");
@@ -115,7 +105,7 @@ const getOptions = async () => {
}
const pluginType = getPluginType();
const { form } = getScope();
const input = getInputFromForm(form, pluginType);
const { input, record } = getInputFromForm(form, pluginType);
for (let key in define.input) {
const inWatches = props.watches?.includes(key);
@@ -141,6 +131,7 @@ const getOptions = async () => {
typeName: form.type,
action: props.action,
input,
record,
data: {
searchKey: props.search ? searchKeyRef.value : "",
pageNo,
@@ -211,7 +202,7 @@ watch(
() => {
const pluginType = getPluginType();
const { form, key } = getScope();
const input = getInputFromForm(form, pluginType);
const { input, record } = getInputFromForm(form, pluginType);
const watches: any = {};
if (props.watches && props.watches.length > 0) {
for (const key of props.watches) {
@@ -15,6 +15,7 @@
import { ComponentPropsType, doRequest } from "/@/components/plugins/lib";
import { defineComponent, inject, ref, useAttrs, watch, Ref } from "vue";
import { PluginDefine } from "@certd/pipeline";
import { getInputFromForm } from "./utils";
defineOptions({
name: "RemoteTreeSelect",
@@ -67,7 +68,7 @@ const getOptions = async () => {
}
const pluginType = getPluginType();
const { form } = getScope();
const input = (pluginType === "plugin" ? form?.input : form) || {};
const { input, record } = getInputFromForm(form, pluginType);
for (let key in define.input) {
const inWatches = props.watches?.includes(key);
@@ -98,6 +99,7 @@ const getOptions = async () => {
pageNo,
pageSize,
},
record,
},
{
onError(err: any) {
@@ -0,0 +1,26 @@
import { cloneDeep } from "lodash-es";
export function getInputFromForm(form: any, pluginType: string) {
form = cloneDeep(form);
let input: any = {};
const record: any = form;
if (pluginType === "plugin") {
input = form?.input || {};
delete form.input;
} else if (pluginType === "access") {
input = form?.access || {};
delete form.access;
} else if (pluginType === "notification") {
input = form?.body || {};
delete form.body;
} else if (pluginType === "addon") {
input = form?.body || {};
delete form.body;
} else {
throw new Error(`pluginType ${pluginType} not support`);
}
return {
input,
record,
};
}
@@ -20,6 +20,7 @@ export const Dicts = {
uploaderTypeDict: dict({
data: [
{ label: "SFTP", value: "sftp" },
{ label: "SCP", value: "scp" },
{ label: "FTP", value: "ftp" },
{ label: "阿里云OSS", value: "alioss" },
{ label: "腾讯云COS", value: "tencentcos" },
@@ -12,11 +12,12 @@ export type RequestHandleReq<T = any> = {
action: string;
data?: any;
input: T;
record?: any;
};
export async function doRequest(req: RequestHandleReq, opts: any = {}) {
const url = `/pi/handle/${req.type}`;
const { typeName, action, data, input } = req;
const { typeName, action, data, input, record } = req;
const res = await request({
url,
method: "post",
@@ -25,6 +26,7 @@ export async function doRequest(req: RequestHandleReq, opts: any = {}) {
action,
data,
input,
record,
},
...opts,
});
@@ -160,6 +160,7 @@ export default {
updateTime: "Update Time",
triggerType: "Trigger Type",
pipelineId: "Pipeline Id",
nextRunTime: "Next Run Time",
},
pi: {
@@ -167,6 +167,7 @@ export default {
updateTime: "更新时间",
triggerType: "触发类型",
pipelineId: "流水线Id",
nextRunTime: "下次运行时间",
},
pi: {
validTime: "流水线有效期",
@@ -117,3 +117,4 @@ span.fs-icon-svg {
margin: 0 !important;
}
}
@@ -5,6 +5,7 @@
@import "./fix-windicss.less";
@import "./antdv4.less";
@import "./certd.less";
@import "./dark.less";
html,
body {
@@ -0,0 +1,7 @@
.dark{
.fs-page-header{
.title {
color: #d5d5d5 !important;
}
}
}
@@ -94,7 +94,7 @@ const defaultPreferences: Preferences = {
colorPrimary: "hsl(212 100% 45%)",
colorSuccess: "hsl(144 57% 58%)",
colorWarning: "hsl(42 84% 61%)",
mode: "light",
mode: "auto",
radius: "0.5",
semiDarkHeader: false,
semiDarkSidebar: false,
@@ -25,7 +25,7 @@
</a-form-item>
<a-form-item :label="t('certd.monitor.setting.dnsServer')" :name="['dnsServer']">
<div class="flex">
<a-select v-model:value="formState.dnsServer" mode="tags" :open="false" />
<a-select v-model:value="formState.dnsServer" :token-separators="[' ', ',', '', '', '|']" mode="tags" :open="false" />
</div>
<div class="helper">{{ t("certd.monitor.setting.dnsServerHelper") }}</div>
</a-form-item>
@@ -45,15 +45,16 @@
</template>
<script setup lang="tsx">
import { notification } from "ant-design-vue";
import { merge } from "lodash-es";
import { reactive } from "vue";
import * as api from "./api";
import { UserSiteMonitorSetting } from "./api";
import { notification } from "ant-design-vue";
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 { utils } from "/@/utils";
import NotificationSelector from "/@/views/certd/notification/notification-selector/index.vue";
import { useI18n } from "/src/locales";
import { useSettingStore } from "/src/store/settings";
const { t } = useI18n();
@@ -74,6 +75,7 @@ async function loadUserSettings() {
loadUserSettings();
const doSave = async (form: any) => {
await utils.sleep(1);
await api.SiteMonitorSettingsSave({
...formState,
});
@@ -352,6 +352,7 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
column: {
align: "center",
width: 120,
show: false,
sorter: true,
},
form: {
@@ -464,6 +465,18 @@ export default function ({ crudExpose, context: { selectedRowKeys, openCertApply
align: "center",
},
},
nextRunTime: {
title: t("certd.fields.nextRunTime"),
type: "datetime",
form: {
show: false,
},
column: {
sorter: true,
width: 150,
align: "center",
},
},
disabled: {
title: t("certd.fields.enabled"),
type: "dict-switch",
@@ -37,7 +37,7 @@
<div class="step-row">
<div class="text">
<fs-icon icon="ion:flash"></fs-icon>
<h4 class="title" :class="{ disabled: element.disabled, deleted: element.disabled }">{{ element.title }}</h4>
<h4 class="title" :class="{ disabled: element.disabled, deleted: element.disabled }" :title="element.title">{{ element.title }}</h4>
</div>
<div class="action">
<a key="edit" @click="stepEdit(currentTask, element, index)">编辑</a>
@@ -306,6 +306,9 @@ export default {
justify-content: space-between;
.text {
display: flex;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
> * {
margin: 0px;
margin-right: 15px;
@@ -314,9 +317,16 @@ export default {
.action {
display: flex;
align-items: center;
flex-wrap: nowrap;
word-wrap: nowrap;
margin-left: 10px;
> * {
margin-right: 10px;
font-size: 14px;
display: flex;
align-items: center;
flex-wrap: nowrap;
white-space: nowrap;
}
}
}
@@ -885,6 +885,7 @@ export default defineComponent({
saveLoading.value = false;
}
};
const edit = () => {
pipeline.value = cloneDeep(currentPipeline.value);
currentHistory.value = null;
@@ -36,7 +36,7 @@
<a-collapse v-if="detail?.template?.pipelineId > 0" v-model:active-key="activeKey">
<a-collapse-panel v-for="(step, stepId) in steps" :key="stepId" class="step-item" :header="step.title">
<div class="step-inputs flex flex-wrap">
<div v-for="(input, key) of step.input" :key="key" class="hover:bg-gray-100 p-5 w-full xl:w-[50%]">
<div v-for="(input, key) of step.input" :key="key" class="hover:bg-gray-100 dark:hover:bg-[#2d2d2d] p-5 w-full xl:w-[50%]">
<div class="flex flex-between" :title="input.define.helper">
<div class="flex flex-1 overflow-hidden mr-5">
<span style="min-width: 140px" class="bas">
@@ -7,7 +7,7 @@
</template>
<script setup lang="ts">
import { get, set } from "lodash-es";
import { computed, reactive, ref, defineProps } from "vue";
import { computed, reactive, ref } from "vue";
import { useStepHelper } from "./utils";
import { usePluginStore } from "/@/store/plugin";
@@ -221,14 +221,12 @@ onMounted(() => {
border: 1px solid #e8e8e8;
border-radius: 4px;
overflow: hidden;
background-color: #fff;
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: #fafafa;
border-bottom: 1px solid #e8e8e8;
}
@@ -245,13 +243,11 @@ onMounted(() => {
.input-form {
margin-bottom: 12px;
padding: 12px;
background-color: #fafafa;
border-radius: 4px;
}
.domain-info {
padding: 5.5px 12px;
background-color: #f0f0f0;
border-radius: 4px;
display: flex;
gap: 16px;
@@ -272,7 +268,6 @@ onMounted(() => {
.summary {
margin-top: 16px;
padding: 12px;
background-color: #f8f9fa;
border-radius: 4px;
.summary-text {
}
@@ -110,7 +110,6 @@ onMounted(() => {
}
.info-item {
background-color: #fafafa;
border-radius: 4px;
padding: 12px;
@@ -138,7 +138,6 @@ const resultError = computed(() => {
.port-info {
font-size: 12px;
color: #999;
background-color: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
margin-right: 8px;
@@ -154,7 +153,6 @@ const resultError = computed(() => {
.error-message,
.object-result,
.text-result {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
@@ -170,7 +168,6 @@ const resultError = computed(() => {
}
.test-log {
background-color: #f8f8f8;
padding: 8px 10px;
border-radius: 3px;
overflow-x: auto;
@@ -30,7 +30,6 @@ import ServerInfoCard from "./ServerInfoCard.vue";
.page-sys-nettest {
.nettest-container {
padding: 16px;
background-color: #fff;
}
.test-areas {
@@ -68,7 +68,7 @@ export class UserSettingsController extends CrudController<UserSettingsService>
const entity = await this.service.getByKey(key, this.getUserId());
return this.ok(entity);
}
@Post("/grant/get", { summary: Constants.per.authOnly })
@Post("/grant/get", { summary: Constants.per.authOnly })
async grantSettingsGet() {
const userId = this.getUserId();
const setting = await this.service.getSetting<UserGrantSetting>(userId, UserGrantSetting);
@@ -88,6 +88,4 @@ export class UserSettingsController extends CrudController<UserSettingsService>
return this.ok({});
}
}
@@ -35,16 +35,16 @@ export class HandleController extends BaseController {
@Post('/access', { summary: Constants.per.authOnly })
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
const userId = this.getUserId();
let inputAccess = body.input.access;
if (body.input.id > 0) {
const oldEntity = await this.accessService.info(body.input.id);
let inputAccess = body.input;
if (body.record.id > 0) {
const oldEntity = await this.accessService.info(body.record.id);
if (oldEntity) {
if (oldEntity.userId !== this.getUserId()) {
throw new Error('access not found');
}
const param: any = {
type: body.typeName,
setting: JSON.stringify(body.input.access),
setting: JSON.stringify(body.input),
};
this.accessService.encryptSetting(param, oldEntity);
inputAccess = this.accessService.decryptAccessEntity(param);
@@ -53,7 +53,7 @@ export class HandleController extends BaseController {
const accessGetter = new AccessGetter(userId, this.accessService.getById.bind(this.accessService));
const access = await newAccess(body.typeName, inputAccess,accessGetter);
mergeUtils.merge(access, body.input);
// mergeUtils.merge(access, body.input);
const res = await access.onRequest(body);
return this.ok(res);
@@ -61,7 +61,7 @@ export class HandleController extends BaseController {
@Post('/notification', { summary: Constants.per.authOnly })
async notificationRequest(@Body(ALL) body: NotificationRequestHandleReq) {
const input = body.input.body;
const input = body.input;
const notification = await newNotification(body.typeName, input, {
http,
@@ -58,6 +58,8 @@ export class PipelineEntity {
// 变量
lastVars: any;
nextRunTime: number;
@Column({name: 'order', comment: '排序', nullable: true,})
order: number;
@@ -69,4 +71,5 @@ export class PipelineEntity {
default: () => 'CURRENT_TIMESTAMP',
})
updateTime: Date;
}
@@ -48,7 +48,7 @@ import { TaskServiceBuilder } from "./getter/task-service-getter.js";
import { nanoid } from "nanoid";
import { set } from "lodash-es";
import { executorQueue } from "@certd/lib-server";
import parser from "cron-parser";
const runningTasks: Map<string | number, Executor> = new Map();
@@ -141,16 +141,47 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
// @ts-ignore
item.stepCount = stepCount;
if(item.triggerCount == 0 ){
if (item.triggerCount == 0) {
item.triggerCount = pipeline.triggers?.length;
}
//获取下次执行时间
if (pipeline.triggers?.length > 0) {
const triggers = pipeline.triggers.filter((item) => item.type === 'timer');
if (triggers && triggers.length > 0) {
let nextTimes: any = [];
for (const item of triggers) {
if (!item.props?.cron) {
continue;
}
const ret = this.getCronNextTimes(item.props?.cron, 1);
nextTimes.push(...ret);
}
item.nextRunTime = nextTimes[0]
}
}
delete item.content;
}
return result;
}
getCronNextTimes(cron: string, count: number = 1) {
if (cron == null) {
return [];
}
const nextTimes = [];
const interval = parser.parseExpression(cron);
for (let i = 0; i < count; i++) {
const next = interval.next().getTime();
nextTimes.push(dayjs(next).format("YYYY-MM-DD HH:mm:ss"));
}
return nextTimes;
}
private async fillLastVars(records: PipelineEntity[]) {
const pipelineIds: number[] = [];
const recordMap = {};
@@ -220,7 +251,7 @@ export class PipelineService extends BaseService<PipelineEntity> {
//修改
old = await this.info(bean.id);
}
if (!old || !old.webhookKey ) {
if (!old || !old.webhookKey) {
bean.webhookKey = await this.genWebhookKey();
}
@@ -1,3 +1,4 @@
export * from './plugin-restart.js';
export * from './plugin-script.js';
export * from './plugin-db-backup.js';
export * from './plugin-deploy-to-certd.js';
@@ -34,8 +34,8 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
name: "a-select",
options: [
{ label: "本地复制", value: "local" },
{ label: "ssh上传", value: "ssh" },
{ label: "oss上传", value: "oss" },
{ label: "oss上传(推荐)", value: "oss" },
{ label: "ssh上传(请使用oss上传方式)", value: "ssh", disabled: true },
],
placeholder: "",
},
@@ -72,6 +72,7 @@ export class DBBackupPlugin extends AbstractPlusTaskPlugin {
{ value: "tencentcos", label: "腾讯云COS" },
{ value: "ftp", label: "Ftp" },
{ value: "sftp", label: "Sftp" },
{ value: "scp", label: "SCP" },
],
},
mergeScript: `
@@ -0,0 +1,76 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput } from '@certd/pipeline';
import { httpsServer } from '../../modules/auto/https/server.js';
import { RestartCertdPlugin } from './plugin-restart.js';
import path from 'path';
import fs from 'fs';
import { CertApplyPluginNames, CertInfo, CertReader } from '@certd/plugin-lib';
@IsTaskPlugin({
name: 'DeployToCertd',
title: '部署证书到Certd本身',
icon: 'mdi:restart',
desc: '【仅管理员可用】 部署证书到 certd的https服务,用于更新 Certd 的 ssl 证书,建议将此任务放在流水线的最后一步',
group: pluginGroups.admin.key,
onlyAdmin: true,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class DeployToCertdPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'output-selector',
from: [...CertApplyPluginNames],
},
required: true,
})
cert!: CertInfo;
async onInstance() { }
async execute(): Promise<void> {
if (!this.isAdmin()) {
throw new Error('只有管理员才能运行此任务');
}
//部署证书
let crtPath = "ssl/cert.crt";
let keyPath = "ssl/cert.key";
const certReader = new CertReader(this.cert);
const dataDir = "./data";
const handle = async ({ tmpCrtPath, tmpKeyPath, }) => {
this.logger.info('复制到目标路径');
if (crtPath) {
crtPath = crtPath.startsWith('/') ? crtPath : path.join(dataDir, crtPath);
this.copyFile(tmpCrtPath, crtPath);
}
if (keyPath) {
keyPath = keyPath.trim();
keyPath = keyPath.startsWith('/') ? keyPath : path.join(dataDir, keyPath);
this.copyFile(tmpKeyPath, keyPath);
}
};
await certReader.readCertFile({ logger: this.logger, handle });
this.logger.info(`证书已部署到 ${crtPath}${keyPath}`);
this.logger.info('Certd https server 将在 30 秒后重启');
await this.ctx.utils.sleep(30000);
await httpsServer.restart();
}
copyFile(srcFile: string, destFile: string) {
this.logger.info(`复制文件:${srcFile} => ${destFile}`);
const dir = path.dirname(destFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.copyFileSync(srcFile, destFile);
}
}
new RestartCertdPlugin();
@@ -316,6 +316,8 @@ export class AliyunDeployCertToALB extends AbstractTaskPlugin {
certId = certIdRes.certId as any;
}else if (casCert.certId){
certId = casCert.certId;
}else{
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
}
@@ -141,11 +141,13 @@ export class DeployCertToAliyunApig extends AbstractTaskPlugin {
const casCert = this.cert as CasCertId;
if (casCert.certId) {
certId = casCert.certId;
} else {
} else if (certInfo.crt) {
certId = await sslClient.uploadCert({
name: this.buildCertName(CertReader.getMainDomain(certInfo.crt)),
cert: certInfo,
});
}else{
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
}
@@ -117,13 +117,15 @@ export class DeployCertToAliyunCDN extends AbstractTaskPlugin {
const casCert = this.cert as CasCertId;
if (casCert.certId) {
certId = casCert.certId;
} else {
} else if (certInfo.crt) {
certName = this.buildCertName(CertReader.getMainDomain(certInfo.crt))
const certIdRes = await sslClient.uploadCertificate({
name:certName,
cert: certInfo,
});
certId = certIdRes.certId as any;
}else{
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
}
@@ -106,6 +106,7 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
let certId: any = this.cert
if (typeof this.cert === 'object') {
const certInfo = this.cert as CertInfo;
const casCertId = this.cert as CasCertId;
if (certInfo.crt) {
this.logger.info('上传证书:', CertName);
const cert: any = this.cert;
@@ -117,6 +118,10 @@ export class DeployCertToAliyunDCDN extends AbstractTaskPlugin {
SSLPub: cert.crt,
SSLPri: cert.key,
};
}else if (casCertId.certId){
certId = casCertId.certId;
}else{
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
}
this.logger.info('使用已上传的证书:', certId);
@@ -122,7 +122,7 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
if (casCert.certId) {
certId = casCert.certId;
certName = casCert.certName;
} else {
} else if (certInfo.crt) {
certName = this.buildCertName(CertReader.getMainDomain(certInfo.crt));
const certIdRes = await sslClient.uploadCertificate({
@@ -131,6 +131,8 @@ export class AliyunDeployCertToESA extends AbstractTaskPlugin {
});
certId = certIdRes.certId as any;
this.logger.info("上传证书成功", certId, certName);
}else{
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
}
return {
@@ -175,7 +175,7 @@ export class DeployCertToAliyunOSS extends AbstractTaskPlugin {
<PrivateKey>${certInfo.key}</PrivateKey>
<Certificate>${certInfo.crt}</Certificate>
`
}else{
}else {
const casCert = this.cert as CasCertId;
certStr = `<CertId>${casCert.certIdentifier}</CertId>`
}
@@ -153,15 +153,17 @@ export class AliyunDeployCertToWaf extends AbstractTaskPlugin {
});
const cert = this.cert as CertInfo;
const casCert = this.cert as CasCertInfo;
if (cert.crt) {
const certIdRes = await sslClient.uploadCertificate({
name: this.buildCertName(CertReader.getMainDomain(cert.crt)),
cert: cert,
});
certId = certIdRes.certId as any;
}else {
const casCert = this.cert as CasCertInfo;
} else if (casCert.certId) {
certId = casCert.certId;
} else {
throw new Error('证书格式错误'+JSON.stringify(this.cert));
}
}
@@ -101,7 +101,7 @@ export class UploadCertToAliyun extends AbstractTaskPlugin {
let certName = ""
if (this.name){
certName = this.appendTimeSuffix(this.name)
}else{
}else {
certName = this.buildCertName(CertReader.getMainDomain(this.cert.crt))
}
const certIdRes = await client.uploadCertificate({
@@ -63,7 +63,10 @@ export class JDCloudDnsProvider extends AbstractDnsProvider {
domainId: domainId
};
}catch (e) {
this.logger.error(e)
if (e.error){
this.logger.error(JSON.stringify(e.error))
throw new Error(JSON.stringify(e.error))
}
throw e
}
@@ -23,6 +23,9 @@ export class OssClientFactory {
} else if (type === "s3") {
const module = await import("./impls/s3.js");
return module.default;
} else if (type === "scp") {
const module = await import("./impls/scp.js");
return module.default;
} else {
throw new Error(`暂不支持此文件上传方式: ${type}`);
}
@@ -0,0 +1,7 @@
import SftpOssClientImpl from "./sftp.js";
export default class ScpOssClientImpl extends SftpOssClientImpl {
getUploaderType() {
return 'scp';
}
}
@@ -5,6 +5,9 @@ import fs from "fs";
import { SftpAccess, SshAccess, SshClient } from "../../ssh/index.js";
export default class SftpOssClientImpl extends BaseOssClient<SftpAccess> {
getUploaderType() {
return 'sftp';
}
async download(fileName: string, savePath: string): Promise<void> {
const path = this.join(this.rootDir, fileName);
const client = new SshClient(this.logger);
@@ -48,6 +51,7 @@ export default class SftpOssClientImpl extends BaseOssClient<SftpAccess> {
const key = this.join(this.rootDir, filePath);
try {
const client = new SshClient(this.logger);
const uploaderType = this.getUploaderType();
await client.uploadFiles({
connectConf: access,
mkdirs: true,
@@ -57,7 +61,7 @@ export default class SftpOssClientImpl extends BaseOssClient<SftpAccess> {
remotePath: key,
},
],
uploadType: "sftp",
uploadType: uploaderType,
opts: {
mode: this.access?.fileMode ?? undefined,
},
@@ -125,7 +125,7 @@ export class OnePanelClient {
async getAccessToken() {
if (this.access.type === "apikey") {
return this.getAccessTokenByApiKey();
return await this.getAccessTokenByApiKey();
} else {
return await this.getAccessTokenByPassword();
}
@@ -7,9 +7,9 @@ import { createCertDomainGetterInputDefine, createRemoteSelectInputDefine } from
@IsTaskPlugin({
name: "SafelineDeployToWebsitePlugin",
title: "雷池-更新证书",
title: "雷池-更新证书(支持控制台和防护应用)",
icon: "svg:icon-safeline",
desc: "更新长亭雷池WAF的证书",
desc: "更新长亭雷池WAF的证书,支持更新控制台和防护应用的证书。",
group: pluginGroups.panel.key,
default: {
strategy: {
@@ -51,7 +51,7 @@ export class SafelineDeployToWebsitePlugin extends AbstractTaskPlugin {
title: "雷池证书",
typeName: "SafelineDeployToWebsitePlugin",
action: SafelineDeployToWebsitePlugin.prototype.onGetCertIds.name,
helper: "请选择要更新的雷池的证书Id,需要先手动到雷池控制台上传一次",
helper: "请选择要更新的雷池的证书Id,需要先手动到雷池控制台上传一次\n如果输入0,则表示新增证书,运行一次之后可以在雷池中使用该证书,最后记得在此处选择新上传的这个证书id,后续将进行自动更新",
required: true,
})
)
@@ -69,19 +69,26 @@ export class SafelineDeployToWebsitePlugin extends AbstractTaskPlugin {
}
async uploadCert(certId: number) {
await this.doRequest({
const data:any = {
manual: {
crt: this.cert.crt,
key: this.cert.key,
},
type: 2,
};
let type = "新增"
// @ts-ignore
if (certId !== "0" && certId >0) {
//@ts-ignore
data.id = parseInt(certId)
type = "更新"
}
const res = await this.doRequest({
url: "/api/open/cert",
method: "post",
data: {
id: certId,
manual: {
crt: this.cert.crt,
key: this.cert.key,
},
type: 2,
},
data:data
});
this.logger.info(`证书<${certId}>更新成功`);
this.logger.info(`证书<${certId}>${type}成功,ID:${res}`);
}
async doRequest(config: HttpRequestConfig<any>) {
@@ -109,7 +116,7 @@ export class SafelineDeployToWebsitePlugin extends AbstractTaskPlugin {
});
const nodes = res?.nodes;
if (!nodes || nodes.length === 0) {
throw new Error("没有找到证书,请先在雷池控制台中手动上传证书,并关联防护站点,后续才可以自动更新");
throw new Error("没有找到证书,请先在雷池控制台中手动上传证书,并关联防护站点或控制台面板使用该证书,后续才可以自动更新");
}
const options = nodes.map(item => {
return {
@@ -1,8 +1,8 @@
import { IsAccess, AccessInput, BaseAccess } from '@certd/pipeline';
import { AccessInput, BaseAccess, IsAccess, PageSearch } from '@certd/pipeline';
/**
*
* certd的后台管理系统中
https://www.dns.com.cn/login/toLogin.do
API接口https://api.bizcn.com/rrpservices
*/
@IsAccess({
name: 'xinnetconnect',
@@ -38,6 +38,23 @@ export class XinnetConnectAccess extends BaseAccess {
password = '';
async getDomainList(req: PageSearch): Promise<any> {
let bodyXml =`
<limit>${req.pageSize}</limit>
<offset>${req.pageNo}</offset>
`
if(req.searchKey){
bodyXml += `<domainname>${req.searchKey}</domainname>`
}
const res = await this.doRequest({
url: "/domainService",
bodyXml: bodyXml,
service: "getDomainList",
})
return res
}
async addDnsRecord(req: {domain:string,hostRecord:string, value:string, type:string}): Promise<any> {
const { domain,hostRecord, value, type } = req;
@@ -125,15 +142,19 @@ export class XinnetConnectAccess extends BaseAccess {
// 提取返回结果
const soapBody = result['soap:Envelope']['soap:Body'];
const addDnsRecordResponse = soapBody["ns1:addDnsRecordResponse"];
console.log(addDnsRecordResponse)
const keys = Object.keys(soapBody);
if (keys.length === 0) {
throw new Error('SOAP响应体为空');
}
const addDnsRecordResponse = soapBody[keys[0]];
this.ctx.logger.info(addDnsRecordResponse)
const resultData = addDnsRecordResponse.response.result;
const res = {
code: resultData.$.code,
msg: resultData.msg
}
console.log('操作结果:', res);
this.ctx.logger.info('操作结果:', res);
if (res.code != "200") {
throw new Error(res.msg + " code:" + res.code);
@@ -1,4 +1,5 @@
import { AbstractDnsProvider, CreateRecordOptions, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { PageRes, PageSearch } from "@certd/pipeline";
import { AbstractDnsProvider, CreateRecordOptions, DomainRecord, IsDnsProvider, RemoveRecordOptions } from "@certd/plugin-cert";
import { XinnetConnectAccess } from "./access.js";
@@ -62,6 +63,21 @@ export class XinnetConnectDnsProvider extends AbstractDnsProvider<XinnetConnectR
await this.access.delDnsRecord(record)
this.logger.info(`删除域名解析成功:fullRecord=${fullRecord}`);
}
async getDomainListPage(req: PageSearch): Promise<PageRes<DomainRecord>> {
const res = await this.access.getDomainList(req)
let list = res.domainlist || []
list = list.map(item => ({
domain: item.domain,
id: item.domain,
}))
return {
pageNo: req.pageNo,
pageSize: req.pageSize,
total: res.total || 0,
list,
}
}
}
//实例化这个provider,将其自动注册到系统中
+1 -1
View File
@@ -1 +1 @@
02:29
23:12
+1 -1
View File
@@ -1 +1 @@
02:51
02:18